Skip to content

feat(search): ✨ board and post #33

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions app/[slug]/[board]/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import { getServerSession } from "next-auth";

import { authOptions } from "@/lib/auth";
Expand All @@ -11,6 +12,7 @@ import { CreatePost } from "@/components/posts/create";
import { Input } from "@/components/ui/input";
import { BoardOptions } from "@/components/boards/options";
import { Board } from "@/types/board";
import Search from "../../search";

import PrivateBoard from "../../private";

Expand Down Expand Up @@ -53,13 +55,12 @@ export default async function BoardLayout({
</Badge>
</div>
<div className="flex flex-col items-start space-y-2 sm:flex-row sm:items-center sm:space-x-4 sm:space-y-0">
<Input
disabled
className="w-full sm:w-auto"
placeholder="Search Posts (Coming Soon)"
<Search
paramName="search-post"
placeHolderValue="Search Posts..."
/>
<BoardView />
<BoardOptions />
{hasAccess && <BoardOptions />}
{session ? (
<CreatePost
boardId={board.id as string}
Expand Down
1 change: 0 additions & 1 deletion app/[slug]/[board]/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export default async function BoardPage({
| null;
const session = await getServerSession(authOptions);
const view = searchParams.view || "list";

const hasAccess = await checkUserAccess({
userId: session?.user.id,
projectId: board?.projectId as string,
Expand Down
9 changes: 5 additions & 4 deletions app/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { BoardFilter } from "@/components/boards/filter";

import NotFound from "./not-found";
import PrivateBoard from "./private";
import Search from "./search";

// meta data
export async function generateMetadata({
Expand All @@ -31,6 +32,7 @@ export async function generateMetadata({

export default async function ProjectPage({
params,
searchParams,
}: {
params: { slug: string };
}) {
Expand All @@ -56,10 +58,9 @@ export default async function ProjectPage({
<header className="mb-8 flex flex-col items-center justify-between gap-4 sm:flex-row">
{session && hasAccess && (
<>
<Input
disabled
className="w-full sm:w-auto"
placeholder="Search boards... (Coming Soon)"
<Search
paramName="search-board"
placeHolderValue="Search Boards"
/>
<section className="flex w-full flex-wrap items-center justify-center gap-2 sm:w-auto sm:justify-end">
<BoardFilter />
Expand Down
47 changes: 47 additions & 0 deletions app/[slug]/search.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";
import React, { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Input } from "@/components/ui/input";
import { debounce } from "@/utils/debounce";
const Search = ({
paramName,
placeHolderValue,
}: {
paramName: string;
placeHolderValue: string;
}) => {
const [searchQuery, setSearchQuery] = React.useState<string>("");
const router = useRouter();
const searchParams = useSearchParams();
const SearchQueryUpdate = debounce((searchQuery) => {
const params = new URLSearchParams(searchParams);
if (searchQuery) {
params.set(paramName, searchQuery);
} else {
params.delete(paramName);
}
router.push(`?${params}`, { scroll: false });
}, 300);

// retrieving state from url, could be useful to share the filtered posts
useEffect(() => {
setSearchQuery(searchParams.get("search") || "");
}, []);

// calling filter upon change in search keyword
useEffect(() => {
SearchQueryUpdate(searchQuery);
}, [searchQuery]);
return (
<div>
<Input
className="w-full sm:w-auto"
placeholder={placeHolderValue}
onChange={(e) => setSearchQuery(e.target.value as string)}
value={searchQuery}
/>
</div>
);
};

export default Search;
1 change: 1 addition & 0 deletions components/boards/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export const CreateBoardForm = ({
onSuccess: () => {
toast.success("Board created successfully");
queryClient.invalidateQueries({ queryKey: ["boards"] });
form.reset();
setOpen(false);
},
onError: (error) => {
Expand Down
59 changes: 40 additions & 19 deletions components/boards/list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useState } from "react";
import { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Board } from "@prisma/client";
import Link from "next/link";
Expand All @@ -9,6 +9,7 @@ import _ from "lodash";
import { useSession } from "next-auth/react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { motion } from "framer-motion";
import Fuse from "fuse.js";

import { Separator } from "../ui/separator";
import { Badge } from "../ui/badge";
Expand All @@ -33,10 +34,13 @@ export const BoardsList = ({
limit?: number;
}) => {
const [showAllBoards, setShowAllBoards] = useState(showAll);
const [boards, setBoards] = useState<Board[]>([]);
const [filteredBoards, setFilteredBoards] = useState<Board[]>([]);
const searchParams = useSearchParams();
const { data: session } = useSession();
const sortValue = session ? searchParams.get("sort") || "" : "";
const visibilityValue = session ? searchParams.get("visibility") || "" : "";
const searchKeyword = searchParams.get("search-board") || "";

const { data, isLoading } = useQuery<{ boards: Board[] }>({
queryKey: ["boards"],
Expand All @@ -51,6 +55,41 @@ export const BoardsList = ({
},
});

useEffect(() => {
if (data?.boards) {
setBoards(data?.boards);
}
}, [data?.boards]);

useEffect(() => {
let updatedBoards = [...boards];
if (searchKeyword && updatedBoards.length > 0) {
const fuseOptions = {
keys: ["name", "description"],
};

const fuse = new Fuse(updatedBoards, fuseOptions);
const results = fuse.search(searchKeyword);
updatedBoards = results.map((result) => result.item);
}
if (session && updatedBoards.length > 0) {
// Apply visibility filters
if (visibilityValue === "private") {
updatedBoards = updatedBoards.filter((board) => board.isPrivate);
} else if (visibilityValue === "public") {
updatedBoards = updatedBoards.filter((board) => !board.isPrivate);
}

// Apply sorting
if (sortValue === "z-a") {
updatedBoards = _.orderBy(updatedBoards, ["name"], ["desc"]);
} else if (sortValue === "a-z") {
updatedBoards = _.orderBy(updatedBoards, ["name"], ["asc"]);
}
}
setFilteredBoards(updatedBoards);
}, [boards, searchKeyword, visibilityValue, sortValue]);

if (isLoading) {
return (
<div
Expand All @@ -67,24 +106,6 @@ export const BoardsList = ({
);
}

let filteredBoards = data?.boards || [];

if (session) {
// Apply visibility filters
if (visibilityValue === "private") {
filteredBoards = filteredBoards.filter((board) => board.isPrivate);
} else if (visibilityValue === "public") {
filteredBoards = filteredBoards.filter((board) => !board.isPrivate);
}

// Apply sorting
if (sortValue === "z-a") {
filteredBoards = _.orderBy(filteredBoards, ["name"], ["desc"]);
} else if (sortValue === "a-z") {
filteredBoards = _.orderBy(filteredBoards, ["name"], ["asc"]);
}
}

const displayedBoards = showAllBoards
? filteredBoards
: showAllBoards
Expand Down
1 change: 1 addition & 0 deletions components/posts/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const CreatePostSheet = ({
onSuccess: () => {
toast.success("Feature Request Added");
queryClient.invalidateQueries({ queryKey: ["posts", boardId] });
form.reset();
setOpen(false);
},
onError: (error) => {
Expand Down
36 changes: 29 additions & 7 deletions components/posts/list.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import React from "react";
import React, { useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
import { BoardPostType, PostStatus } from "@prisma/client";
import { JsonValue } from "@prisma/client/runtime/library";
import { motion } from "framer-motion";
import Fuse from "fuse.js";

import Spinner from "@/components/common/spinner";

Expand Down Expand Up @@ -54,6 +56,9 @@ export function PostsList({
cols = 2,
hasAccess,
}: PostsListProps) {
const [isSearching, setIsSearching] = useState(false);
const searchParams = useSearchParams();
const searchKeyword = searchParams.get("search-post") || "";
const { data, isLoading } = useQuery<{ posts: Post[] }>({
queryKey: ["posts", boardId],
queryFn: async () => {
Expand All @@ -67,18 +72,35 @@ export function PostsList({
},
});

if (isLoading) {
const posts = data?.posts || [];
const [filteredPosts, setFilteredPosts] = useState(posts);

useEffect(() => {
if (searchKeyword) {
setIsSearching(true);
const fuseOptions = {
keys: ["title", "description"],
};

const fuse = new Fuse(posts, fuseOptions);
const results = fuse.search(searchKeyword);

setIsSearching(false);
setFilteredPosts(results.map((result) => result.item));
} else {
setFilteredPosts(posts);
}
}, [searchKeyword, data]);
if (isLoading || isSearching) {
return (
<div className="flex h-64 items-center justify-center">
<Spinner />
</div>
);
}

const posts = data?.posts || [];

// Sort posts by createdAt in descending order
const sortedPosts = [...posts].sort(
const sortedPosts = [...filteredPosts].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);

Expand Down Expand Up @@ -111,10 +133,10 @@ export function PostsList({
currentUserId={currentUserId}
hasAccess={hasAccess}
layout={view}
// @ts-ignore
// @ts-expect-error: will improve ts later
post={post}
postType={post.postType}
// @ts-ignore
// @ts-expect-error: will improve ts later
user={post.user!}
/>
</Link>
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"cmdk": "1.0.0",
"date-fns": "4.0.0-beta.1",
"framer-motion": "~11.1.1",
"fuse.js": "^7.0.0",
"geist": "^1.3.1",
"intl-messageformat": "^10.5.0",
"jsonwebtoken": "^9.0.2",
Expand Down