starting refinement
This commit is contained in:
86
src/components/blog/Card.tsx
Normal file
86
src/components/blog/Card.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Show } from "solid-js";
|
||||
import CardLinks from "./CardLinks";
|
||||
import DeletePostButton from "./DeletePostButton";
|
||||
|
||||
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 {
|
||||
post: Post;
|
||||
privilegeLevel: "anonymous" | "admin" | "user";
|
||||
linkTarget: "blog" | "project";
|
||||
}
|
||||
|
||||
export default function Card(props: CardProps) {
|
||||
return (
|
||||
<div class="relative z-0 mx-auto h-96 w-full overflow-hidden rounded-lg bg-white shadow-lg dark:bg-zinc-900 md:w-5/6 lg:w-3/4">
|
||||
<Show when={props.privilegeLevel === "admin"}>
|
||||
<div class="absolute top-0 w-full border-b border-white border-opacity-20 bg-white bg-opacity-40 px-2 py-4 backdrop-blur-md dark:border-black dark:bg-zinc-800 dark:bg-opacity-60 md:px-6">
|
||||
<div class="flex justify-between">
|
||||
<Show when={!props.post.published}>
|
||||
<div class="whitespace-nowrap text-center text-lg text-black dark:text-white">
|
||||
Not Published
|
||||
</div>
|
||||
</Show>
|
||||
<DeletePostButton
|
||||
type={props.linkTarget === "blog" ? "Blog" : "Project"}
|
||||
postID={props.post.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<img
|
||||
src={
|
||||
props.post.banner_photo
|
||||
? props.post.banner_photo
|
||||
: props.linkTarget === "blog"
|
||||
? "/bitcoin.jpg"
|
||||
: "/blueprint.jpg"
|
||||
}
|
||||
alt={props.post.title.replaceAll("_", " ") + " banner"}
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
<div class="absolute bottom-0 w-full border-t border-white border-opacity-20 bg-white bg-opacity-40 px-2 py-4 backdrop-blur-md dark:border-zinc-900 dark:bg-zinc-800 dark:bg-opacity-60 md:px-6">
|
||||
<div class="flex flex-col items-center justify-between md:flex-row">
|
||||
<div class="text-center md:text-left">
|
||||
<div class="text-lg text-black dark:text-white md:text-xl">
|
||||
{props.post.subtitle}
|
||||
</div>
|
||||
<div class="text-2xl text-black dark:text-white md:text-3xl">
|
||||
{props.post.title.replaceAll("_", " ")}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-around pt-2 md:w-1/3 md:justify-between md:pl-2 md:pt-0">
|
||||
<div class="my-auto md:h-full">
|
||||
<p class="whitespace-nowrap text-sm text-black dark:text-white">
|
||||
{props.post.total_comments || 0} Comments
|
||||
</p>
|
||||
<p class="whitespace-nowrap text-sm text-black dark:text-white">
|
||||
{props.post.total_likes || 0} Likes
|
||||
</p>
|
||||
</div>
|
||||
<CardLinks
|
||||
postTitle={props.post.title}
|
||||
linkTarget={props.linkTarget}
|
||||
privilegeLevel={props.privilegeLevel}
|
||||
postID={props.post.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/components/blog/CardLinks.tsx
Normal file
48
src/components/blog/CardLinks.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||
|
||||
export interface CardLinksProps {
|
||||
postTitle: string;
|
||||
postID: number;
|
||||
linkTarget: string;
|
||||
privilegeLevel: string;
|
||||
}
|
||||
|
||||
export default function CardLinks(props: CardLinksProps) {
|
||||
const [readLoading, setReadLoading] = createSignal(false);
|
||||
const [editLoading, setEditLoading] = createSignal(false);
|
||||
|
||||
return (
|
||||
<div class="flex flex-col">
|
||||
<A
|
||||
href={`/blog/${props.postTitle}`}
|
||||
onClick={() => setReadLoading(true)}
|
||||
class={`${
|
||||
readLoading()
|
||||
? "bg-zinc-400"
|
||||
: props.linkTarget === "project"
|
||||
? "bg-blue-400 hover:bg-blue-500 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||
: "bg-orange-400 hover:bg-orange-500"
|
||||
} mb-1 ml-2 flex rounded px-4 py-2 font-light text-white shadow transition-all duration-300 ease-out active:scale-90`}
|
||||
>
|
||||
<Show when={readLoading()} fallback="Read">
|
||||
<LoadingSpinner height={24} width={24} />
|
||||
</Show>
|
||||
</A>
|
||||
<Show when={props.privilegeLevel === "admin"}>
|
||||
<A
|
||||
href={`/blog/edit/${props.postID}`}
|
||||
onClick={() => setEditLoading(true)}
|
||||
class={`${
|
||||
editLoading() ? "bg-zinc-400" : "bg-green-400 hover:bg-green-500"
|
||||
} ml-2 flex rounded px-4 py-2 font-light text-white shadow transition-all duration-300 ease-out active:scale-90`}
|
||||
>
|
||||
<Show when={editLoading()} fallback="Edit">
|
||||
<LoadingSpinner height={24} width={24} />
|
||||
</Show>
|
||||
</A>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/components/blog/DeletePostButton.tsx
Normal file
39
src/components/blog/DeletePostButton.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { api } from "~/lib/api";
|
||||
import TrashIcon from "~/components/icons/TrashIcon";
|
||||
import LoadingSpinner from "~/components/LoadingSpinner";
|
||||
|
||||
export interface DeletePostButtonProps {
|
||||
type: string;
|
||||
postID: number;
|
||||
}
|
||||
|
||||
export default function DeletePostButton(props: DeletePostButtonProps) {
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
|
||||
const deletePostTrigger = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const affirm = window.confirm("Are you sure you want to delete?");
|
||||
if (affirm) {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.database.deletePost.mutate({ id: props.postID });
|
||||
// Refresh the page after successful deletion
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
alert("Failed to delete post");
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={deletePostTrigger} class="flex w-full justify-end">
|
||||
<button type="submit">
|
||||
<Show when={loading()} fallback={<TrashIcon height={24} width={24} strokeWidth={1.5} />}>
|
||||
<LoadingSpinner height={24} width={24} />
|
||||
</Show>
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
77
src/components/blog/PostSorting.tsx
Normal file
77
src/components/blog/PostSorting.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { For, Show } from "solid-js";
|
||||
import Card, { Post } from "./Card";
|
||||
|
||||
export interface Tag {
|
||||
id: number;
|
||||
value: string;
|
||||
post_id: number;
|
||||
}
|
||||
|
||||
export interface PostSortingProps {
|
||||
posts: Post[];
|
||||
tags: Tag[];
|
||||
privilegeLevel: "anonymous" | "admin" | "user";
|
||||
type: "blog" | "project";
|
||||
filters?: string;
|
||||
sort?: string;
|
||||
}
|
||||
|
||||
export default function PostSorting(props: PostSortingProps) {
|
||||
const postsToFilter = () => {
|
||||
const filterSet = new Set<number>();
|
||||
props.tags.forEach((tag) => {
|
||||
if (props.filters?.split("|").includes(tag.value.slice(1))) {
|
||||
filterSet.add(tag.post_id);
|
||||
}
|
||||
});
|
||||
return filterSet;
|
||||
};
|
||||
|
||||
const filteredPosts = () => {
|
||||
return props.posts.filter((post) => {
|
||||
return !postsToFilter().has(post.id);
|
||||
});
|
||||
};
|
||||
|
||||
const sortedPosts = () => {
|
||||
const posts = filteredPosts();
|
||||
|
||||
switch (props.sort) {
|
||||
case "newest":
|
||||
return [...posts].reverse();
|
||||
case "oldest":
|
||||
return [...posts];
|
||||
case "most liked":
|
||||
return [...posts].sort((a, b) => b.total_likes - a.total_likes);
|
||||
case "most read":
|
||||
return [...posts].sort((a, b) => b.reads - a.reads);
|
||||
case "most comments":
|
||||
return [...posts].sort((a, b) => b.total_comments - a.total_comments);
|
||||
default:
|
||||
return [...posts].reverse();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={!(props.posts.length > 0 && filteredPosts().length === 0)}
|
||||
fallback={
|
||||
<div class="pt-12 text-center text-2xl italic tracking-wide">
|
||||
All posts filtered out!
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<For each={sortedPosts()}>
|
||||
{(post) => (
|
||||
<div class="my-4">
|
||||
<Card
|
||||
post={post}
|
||||
privilegeLevel={props.privilegeLevel}
|
||||
linkTarget={props.type}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
107
src/components/blog/PostSortingSelect.tsx
Normal file
107
src/components/blog/PostSortingSelect.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { createSignal, createEffect, For, Show } from "solid-js";
|
||||
import { useNavigate, useLocation, useSearchParams } from "@solidjs/router";
|
||||
import Check from "~/components/icons/Check";
|
||||
import UpDownArrows from "~/components/icons/UpDownArrows";
|
||||
|
||||
const sorting = [
|
||||
{ val: "Newest" },
|
||||
{ val: "Oldest" },
|
||||
{ val: "Most Liked" },
|
||||
{ val: "Most Read" },
|
||||
{ val: "Most Comments" },
|
||||
];
|
||||
|
||||
export interface PostSortingSelectProps {
|
||||
type: "blog" | "project";
|
||||
}
|
||||
|
||||
export default function PostSortingSelect(props: PostSortingSelectProps) {
|
||||
const [selected, setSelected] = createSignal(sorting[0]);
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const currentFilters = () => searchParams.filter || null;
|
||||
|
||||
createEffect(() => {
|
||||
let newRoute = location.pathname + "?sort=" + selected().val.toLowerCase();
|
||||
if (currentFilters()) {
|
||||
newRoute += "&filter=" + currentFilters();
|
||||
}
|
||||
navigate(newRoute);
|
||||
});
|
||||
|
||||
const handleSelect = (sort: { val: string }) => {
|
||||
setSelected(sort);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="relative z-10 mt-1 w-72">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen())}
|
||||
class={`${
|
||||
props.type === "project"
|
||||
? "focus-visible:border-blue-600 focus-visible:ring-offset-blue-300"
|
||||
: "focus-visible:border-orange-600 focus-visible:ring-offset-orange-300"
|
||||
} relative w-full cursor-default rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 dark:bg-zinc-900 sm:text-sm`}
|
||||
>
|
||||
<span class="block truncate">{selected().val}</span>
|
||||
<span class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<UpDownArrows
|
||||
strokeWidth={1.5}
|
||||
height={24}
|
||||
width={24}
|
||||
class="fill-zinc-900 dark:fill-white"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Show when={isOpen()}>
|
||||
<div class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-zinc-900 sm:text-sm">
|
||||
<For each={sorting}>
|
||||
{(sort) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect(sort)}
|
||||
class={`relative w-full cursor-default select-none py-2 pl-10 pr-4 text-left ${
|
||||
selected().val === sort.val
|
||||
? props.type === "project"
|
||||
? "bg-blue-100 text-blue-900"
|
||||
: "bg-orange-100 text-orange-900"
|
||||
: "text-zinc-900 dark:text-white hover:bg-zinc-100 dark:hover:bg-zinc-800"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
class={`block truncate ${
|
||||
selected().val === sort.val ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{sort.val}
|
||||
</span>
|
||||
<Show when={selected().val === sort.val}>
|
||||
<span
|
||||
class={`${
|
||||
props.type === "project"
|
||||
? "text-blue-600"
|
||||
: "text-orange-600"
|
||||
} absolute inset-y-0 left-0 flex items-center pl-3`}
|
||||
>
|
||||
<Check
|
||||
strokeWidth={1}
|
||||
height={24}
|
||||
width={24}
|
||||
class="stroke-zinc-900 dark:stroke-white"
|
||||
/>
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
src/components/blog/SessionDependantLike.tsx
Normal file
137
src/components/blog/SessionDependantLike.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { api } from "~/lib/api";
|
||||
import LikeIcon from "~/components/icons/LikeIcon";
|
||||
|
||||
export interface PostLike {
|
||||
id: number;
|
||||
user_id: string;
|
||||
post_id: string;
|
||||
}
|
||||
|
||||
export interface SessionDependantLikeProps {
|
||||
currentUserID: string | undefined | null;
|
||||
privilegeLevel: "admin" | "user" | "anonymous";
|
||||
likes: PostLike[];
|
||||
type: "blog" | "project";
|
||||
projectID: number;
|
||||
}
|
||||
|
||||
export default function SessionDependantLike(props: SessionDependantLikeProps) {
|
||||
const [hovering, setHovering] = createSignal(false);
|
||||
const [likes, setLikes] = createSignal(props.likes);
|
||||
const [instantOffset, setInstantOffset] = createSignal(0);
|
||||
const [hasLiked, setHasLiked] = createSignal(
|
||||
props.likes.some((like) => like.user_id === props.currentUserID)
|
||||
);
|
||||
|
||||
const giveProjectLike = async () => {
|
||||
if (!props.currentUserID) return;
|
||||
|
||||
const initialHasLiked = hasLiked();
|
||||
const initialInstantOffset = initialHasLiked ? -1 : 1;
|
||||
|
||||
setHasLiked(!hasLiked());
|
||||
setInstantOffset(initialInstantOffset);
|
||||
|
||||
try {
|
||||
if (initialHasLiked) {
|
||||
const result = await api.database.removePostLike.mutate({
|
||||
user_id: props.currentUserID,
|
||||
post_id: props.projectID.toString(),
|
||||
});
|
||||
setLikes(result.newLikes as PostLike[]);
|
||||
} else {
|
||||
const result = await api.database.addPostLike.mutate({
|
||||
user_id: props.currentUserID,
|
||||
post_id: props.projectID.toString(),
|
||||
});
|
||||
setLikes(result.newLikes as PostLike[]);
|
||||
}
|
||||
setInstantOffset(0);
|
||||
} catch (error) {
|
||||
console.error("There has been a problem with your like operation:", error);
|
||||
setHasLiked(initialHasLiked);
|
||||
setInstantOffset(0);
|
||||
}
|
||||
};
|
||||
|
||||
const likeCount = () => likes().length + instantOffset();
|
||||
|
||||
const getLikeIconColor = () => {
|
||||
if (hasLiked()) {
|
||||
return props.type === "project" ? "fill-blue-400" : "fill-orange-400";
|
||||
}
|
||||
|
||||
if (hovering()) {
|
||||
return props.type === "project"
|
||||
? "fill-blue-400 dark:fill-blue-600"
|
||||
: "fill-orange-400 dark:fill-orange-500";
|
||||
}
|
||||
|
||||
return "fill-black dark:fill-white";
|
||||
};
|
||||
|
||||
return (
|
||||
<Show
|
||||
when={props.privilegeLevel !== "anonymous"}
|
||||
fallback={
|
||||
<button class="tooltip flex flex-col">
|
||||
<div class="mx-auto">
|
||||
<LikeIcon
|
||||
strokeWidth={1}
|
||||
color="fill-black dark:fill-white"
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</div>
|
||||
<div class="my-auto pl-2 pt-0.5 text-sm text-black dark:text-white">
|
||||
{likes().length} {likes().length === 1 ? "Like" : "Likes"}
|
||||
</div>
|
||||
<div class="tooltip-text -ml-12 w-12">Must be logged in</div>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => giveProjectLike()}
|
||||
onMouseOver={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
>
|
||||
<div
|
||||
class={`${
|
||||
props.type === "project"
|
||||
? "hover:text-blue-400"
|
||||
: "hover:text-orange-400"
|
||||
} tooltip flex flex-col text-black dark:text-white`}
|
||||
>
|
||||
<div class="mx-auto">
|
||||
<LikeIcon
|
||||
strokeWidth={1}
|
||||
color={getLikeIconColor()}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class={`${
|
||||
hasLiked()
|
||||
? props.type === "project"
|
||||
? "text-blue-400"
|
||||
: "text-orange-400"
|
||||
: ""
|
||||
} mx-auto flex pl-2 transition-colors duration-200 ease-in`}
|
||||
>
|
||||
{likeCount()} {likeCount() === 1 ? "Like" : "Likes"}
|
||||
</div>
|
||||
<div class="tooltip-text -ml-14 w-12 px-2">
|
||||
<Show
|
||||
when={hasLiked()}
|
||||
fallback={<div class="px-2 text-center">Leave a Like</div>}
|
||||
>
|
||||
<div class="px-2">Remove Like</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
97
src/components/blog/TagSelector.tsx
Normal file
97
src/components/blog/TagSelector.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { createSignal, createEffect, For, Show, onCleanup } from "solid-js";
|
||||
import { useNavigate, useLocation, useSearchParams } from "@solidjs/router";
|
||||
|
||||
export interface TagSelectorProps {
|
||||
tagMap: Record<string, number>;
|
||||
category: "blog" | "project";
|
||||
}
|
||||
|
||||
export default function TagSelector(props: TagSelectorProps) {
|
||||
const [showingMenu, setShowingMenu] = createSignal(false);
|
||||
let buttonRef: HTMLButtonElement | undefined;
|
||||
let menuRef: HTMLDivElement | undefined;
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const currentSort = () => searchParams.sort || "";
|
||||
const currentFilters = () => searchParams.filter?.split("|") || [];
|
||||
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
buttonRef && menuRef &&
|
||||
!buttonRef.contains(e.target as Node) &&
|
||||
!menuRef.contains(e.target as Node)
|
||||
) {
|
||||
setShowingMenu(false);
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (showingMenu()) {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
onCleanup(() => document.removeEventListener("click", handleClickOutside));
|
||||
}
|
||||
});
|
||||
|
||||
const toggleMenu = () => {
|
||||
setShowingMenu(!showingMenu());
|
||||
};
|
||||
|
||||
const handleCheck = (filter: string, isChecked: boolean) => {
|
||||
if (isChecked) {
|
||||
const newFilters = searchParams.filter?.replace(filter + "|", "");
|
||||
if (newFilters && newFilters.length >= 1) {
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`);
|
||||
} else {
|
||||
navigate(`${location.pathname}?sort=${currentSort()}`);
|
||||
}
|
||||
} else {
|
||||
const currentFiltersStr = searchParams.filter;
|
||||
if (currentFiltersStr) {
|
||||
const newFilters = currentFiltersStr + filter + "|";
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${newFilters}`);
|
||||
} else {
|
||||
navigate(`${location.pathname}?sort=${currentSort()}&filter=${filter}|`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={toggleMenu}
|
||||
class={`${
|
||||
props.category === "project"
|
||||
? "border-blue-500 bg-blue-400 hover:bg-blue-500 dark:border-blue-700 dark:bg-blue-700 dark:hover:bg-blue-800"
|
||||
: "border-orange-500 bg-orange-400 hover:bg-orange-500 dark:border-orange-600 dark:bg-orange-600 dark:hover:bg-orange-700"
|
||||
} mt-2 rounded border px-4 py-2 font-light text-white shadow-md transition-all duration-300 ease-in-out active:scale-90 md:mt-0`}
|
||||
>
|
||||
Filters
|
||||
</button>
|
||||
<Show when={showingMenu()}>
|
||||
<div
|
||||
ref={menuRef}
|
||||
class="absolute z-50 mt-12 rounded-lg bg-zinc-100 py-2 pl-2 pr-4 shadow-lg dark:bg-zinc-900"
|
||||
>
|
||||
<For each={Object.entries(props.tagMap)}>
|
||||
{([key, value]) => (
|
||||
<div class="mx-auto my-2 flex">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!currentFilters().includes(key.slice(1))}
|
||||
onChange={(e) => handleCheck(key.slice(1), e.currentTarget.checked)}
|
||||
/>
|
||||
<div class="-mt-0.5 pl-1 text-sm font-normal">
|
||||
{`${key.slice(1)} (${value}) `}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user