feat(ui): add HighlightText component for search match highlighting
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 49s
All checks were successful
Deploy Flowstate to VPS / deploy (push) Successful in 49s
This commit is contained in:
parent
c33fcb9f91
commit
8d46cee892
3 changed files with 77 additions and 8 deletions
|
|
@ -11,6 +11,8 @@ import CardActions from "./CardActions";
|
|||
import CardAvatars from "./CardAvatars";
|
||||
import CardDueDate from "./CardDueDate";
|
||||
import CardPriority from "./CardPriority";
|
||||
import HighlightText from "@/app/components/ui/HighlightText";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Properties for the Card component.
|
||||
|
|
@ -29,8 +31,8 @@ export interface CardProps {
|
|||
}
|
||||
|
||||
/**
|
||||
* Renders an interactive card container handling drag-and-drop actions, loading states,
|
||||
* and assembling modular sub-components for priorities, due dates, avatars, and actions.
|
||||
* Renders an interactive card container supporting search match highlighting, drag-and-drop actions,
|
||||
* loading states, and modular sub-components for priorities, due dates, avatars, and actions.
|
||||
*
|
||||
* @param {CardProps} props - The component props containing the task object, updating status flag, and status change handler.
|
||||
* @returns {JSX.Element} The rendered card component.
|
||||
|
|
@ -41,6 +43,9 @@ export default function Card({
|
|||
onStatusChange,
|
||||
onDelete,
|
||||
}: CardProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchQuery = searchParams.get("search") || "";
|
||||
|
||||
/**
|
||||
* Initiates the drag action on a task card if not currently updating, storing its ID and status payload.
|
||||
*
|
||||
|
|
@ -87,14 +92,14 @@ export default function Card({
|
|||
{/* --- Card Header --- */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h3 className="font-semibold leading-snug group-hover/card:text-primary transition-colors line-clamp-2">
|
||||
{task.title}
|
||||
<HighlightText text={task.title} query={searchQuery} />
|
||||
</h3>
|
||||
<CardPriority priority={task.priority} />
|
||||
</div>
|
||||
|
||||
{/* Card Description */}
|
||||
<p className="text-sm leading-relaxed text-foreground-muted line-clamp-3">
|
||||
{task.description}
|
||||
<HighlightText text={task.description} query={searchQuery} />
|
||||
</p>
|
||||
|
||||
{/* --- Card Footer --- */}
|
||||
|
|
|
|||
|
|
@ -8,16 +8,21 @@
|
|||
import { Task, PRIORITY_CONFIG } from "@/types/task";
|
||||
import { Calendar } from "lucide-react";
|
||||
import TaskActionButton from "./components/TaskActionButton";
|
||||
import HighlightText from "@/app/components/ui/HighlightText";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* Renders a list of deleted tasks stored in the trash, featuring priority badges,
|
||||
* deletion dates, and action controls for permanent deletion or restoration.
|
||||
* Renders a list of deleted tasks stored in the trash with search term highlighting,
|
||||
* featuring priority badges, deletion dates, and action controls for permanent deletion or restoration.
|
||||
*
|
||||
* @param {Object} props - The component props.
|
||||
* @param {Task[]} props.tasks - The array of deleted tasks to render.
|
||||
* @returns {JSX.Element} The rendered trash list component or an empty state placeholder.
|
||||
*/
|
||||
export default function TrashList({ tasks }: { tasks: Task[] }) {
|
||||
const searchParams = useSearchParams();
|
||||
const searchQuery = searchParams.get("search") || "";
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{tasks.length === 0 ? (
|
||||
|
|
@ -54,7 +59,7 @@ export default function TrashList({ tasks }: { tasks: Task[] }) {
|
|||
<div className="flex flex-col gap-2 pl-2 max-w-2xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-base group-hover:text-primary transition-colors line-clamp-1">
|
||||
{task.title}
|
||||
<HighlightText text={task.title} query={searchQuery} />
|
||||
</h3>
|
||||
|
||||
<span
|
||||
|
|
@ -66,7 +71,10 @@ export default function TrashList({ tasks }: { tasks: Task[] }) {
|
|||
|
||||
{task.description && (
|
||||
<p className="text-xs text-foreground-muted line-clamp-1">
|
||||
{task.description}
|
||||
<HighlightText
|
||||
text={task.description}
|
||||
query={searchQuery}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
|
|
|||
56
app/components/ui/HighlightText.tsx
Normal file
56
app/components/ui/HighlightText.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @file components/ui/HighlightText.tsx
|
||||
* @description Component to highlight matching search query terms within a text string.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Properties for the HighlightText component.
|
||||
*
|
||||
* @interface HighlightTextProps
|
||||
* @property {string | null} [text] - The full text string to render and search within.
|
||||
* @property {string} [query] - The search query term to highlight within the text.
|
||||
* @property {string} [className] - Optional CSS classes applied to the wrapping container element.
|
||||
*/
|
||||
interface HighlightTextProps {
|
||||
text?: string | null;
|
||||
query?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a text string with case-insensitive highlighted search query matches wrapped in a marked element.
|
||||
*
|
||||
* @param {HighlightTextProps} props - The component props.
|
||||
* @returns {JSX.Element | null} The rendered text component with highlighted query terms, or null if no text is provided.
|
||||
*/
|
||||
export default function HighlightText({
|
||||
text,
|
||||
query,
|
||||
className = "",
|
||||
}: HighlightTextProps) {
|
||||
if (!text) return null;
|
||||
if (!query || !query.trim()) {
|
||||
return <span className={className}>{text}</span>;
|
||||
}
|
||||
|
||||
// Regular expression with escaping for special characters, case-insensitive ('gi')
|
||||
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const parts = text.split(new RegExp(`(${escapedQuery})`, "gi"));
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{parts.map((part, index) =>
|
||||
part.toLowerCase() === query.toLowerCase() ? (
|
||||
<mark
|
||||
key={index}
|
||||
className="bg-primary/20 text-primary font-semibold px-0.5 rounded"
|
||||
>
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue