2024-03-20 18:00:23 +00:00
|
|
|
import React, { ComponentProps } from 'react';
|
|
|
|
import { ScrollArea } from './ui/scroll-area';
|
|
|
|
import { cn } from '@/utils/style';
|
|
|
|
import { Badge } from './ui/badge';
|
|
|
|
import { useNavigate, useRouterState } from '@tanstack/react-router';
|
2024-03-22 16:06:56 +00:00
|
|
|
import { LuSearch } from 'react-icons/lu';
|
|
|
|
import { Input } from './ui/input';
|
|
|
|
import { useFuseSearch } from '@/hooks/useFuseSearch';
|
2024-04-01 17:12:44 +00:00
|
|
|
import { Empty } from 'antd';
|
2024-05-13 12:25:00 +00:00
|
|
|
import { globalEventBus } from '@/utils/event';
|
2024-05-17 11:36:46 +00:00
|
|
|
import { Spinner } from './ui/spinner';
|
2024-03-20 18:00:23 +00:00
|
|
|
|
|
|
|
export interface CommonListItem {
|
|
|
|
id: string;
|
|
|
|
title: string;
|
2024-04-14 07:47:55 +00:00
|
|
|
number?: number;
|
2024-03-23 20:18:45 +00:00
|
|
|
content?: React.ReactNode;
|
2024-03-25 13:53:43 +00:00
|
|
|
tags?: string[];
|
2024-03-20 18:00:23 +00:00
|
|
|
href: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CommonListProps {
|
2024-05-13 12:25:00 +00:00
|
|
|
isLoading?: boolean;
|
2024-03-22 16:06:56 +00:00
|
|
|
hasSearch?: boolean;
|
2024-03-20 18:00:23 +00:00
|
|
|
items: CommonListItem[];
|
|
|
|
}
|
|
|
|
export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
|
|
|
const { location } = useRouterState();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
|
2024-03-22 16:06:56 +00:00
|
|
|
const { searchText, setSearchText, searchResult } = useFuseSearch(
|
|
|
|
props.items,
|
|
|
|
{
|
|
|
|
keys: [
|
|
|
|
{
|
2024-05-14 12:31:04 +00:00
|
|
|
name: 'title',
|
2024-03-22 16:06:56 +00:00
|
|
|
weight: 1,
|
|
|
|
},
|
|
|
|
{
|
2024-05-14 12:31:04 +00:00
|
|
|
name: 'id',
|
|
|
|
weight: 0.6,
|
2024-03-22 16:06:56 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'tags',
|
2024-05-14 12:31:04 +00:00
|
|
|
weight: 0.4,
|
2024-03-22 16:06:56 +00:00
|
|
|
},
|
|
|
|
],
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
const finalList = searchResult ?? props.items;
|
|
|
|
|
2024-03-20 18:00:23 +00:00
|
|
|
return (
|
2024-03-23 20:18:45 +00:00
|
|
|
<div className="flex h-full flex-col">
|
2024-03-22 16:06:56 +00:00
|
|
|
{props.hasSearch && (
|
2024-04-01 16:07:38 +00:00
|
|
|
<div className="bg-background/95 supports-[backdrop-filter]:bg-background/60 px-4 pt-4 backdrop-blur">
|
2024-03-22 16:06:56 +00:00
|
|
|
<form>
|
|
|
|
<div className="relative">
|
2024-03-23 20:18:45 +00:00
|
|
|
<LuSearch className="text-muted-foreground absolute left-2 top-2.5 h-4 w-4" />
|
2024-03-22 16:06:56 +00:00
|
|
|
<Input
|
|
|
|
placeholder="Search"
|
|
|
|
className="pl-8"
|
|
|
|
value={searchText}
|
|
|
|
onChange={(e) => setSearchText(e.target.value)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
|
|
)}
|
2024-03-20 18:00:23 +00:00
|
|
|
|
2024-03-22 16:37:30 +00:00
|
|
|
<ScrollArea className="flex-1">
|
2024-04-01 16:07:38 +00:00
|
|
|
<div className="flex flex-col gap-2 p-4">
|
2024-05-17 11:36:46 +00:00
|
|
|
{props.isLoading && (
|
|
|
|
<div className="flex justify-center py-8">
|
|
|
|
<Spinner size={24} />
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2024-05-13 12:25:00 +00:00
|
|
|
{finalList.length === 0 && !props.isLoading && <Empty />}
|
2024-04-01 17:12:44 +00:00
|
|
|
|
2024-03-22 16:06:56 +00:00
|
|
|
{finalList.map((item) => {
|
|
|
|
const isSelected = item.href === location.pathname;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
key={item.id}
|
|
|
|
className={cn(
|
2024-03-23 20:18:45 +00:00
|
|
|
'hover:bg-accent flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all',
|
2024-03-22 16:06:56 +00:00
|
|
|
isSelected && 'bg-muted'
|
|
|
|
)}
|
2024-05-13 12:25:00 +00:00
|
|
|
onClick={() => {
|
|
|
|
globalEventBus.emit('commonListSelected');
|
2024-03-22 16:06:56 +00:00
|
|
|
navigate({
|
|
|
|
to: item.href,
|
2024-05-13 12:25:00 +00:00
|
|
|
});
|
|
|
|
}}
|
2024-03-22 16:06:56 +00:00
|
|
|
>
|
2024-04-14 07:47:55 +00:00
|
|
|
<div className="flex w-full items-center justify-between gap-1">
|
|
|
|
<div className="font-semibold">{item.title}</div>
|
|
|
|
|
|
|
|
{item.number && item.number > 0 && (
|
|
|
|
<span className="opacity-60">{item.number}</span>
|
|
|
|
)}
|
2024-03-20 18:00:23 +00:00
|
|
|
</div>
|
2024-04-01 16:07:38 +00:00
|
|
|
|
|
|
|
{item.content && (
|
|
|
|
<div className="text-muted-foreground line-clamp-2 w-full text-xs">
|
|
|
|
{item.content}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
2024-03-25 13:53:43 +00:00
|
|
|
{Array.isArray(item.tags) && item.tags.length > 0 ? (
|
2024-03-22 16:06:56 +00:00
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
{item.tags.map((tag) => (
|
|
|
|
<Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>
|
|
|
|
{tag}
|
|
|
|
</Badge>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
) : null}
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</ScrollArea>
|
2024-03-22 16:37:30 +00:00
|
|
|
</div>
|
2024-03-20 18:00:23 +00:00
|
|
|
);
|
|
|
|
});
|
|
|
|
CommonList.displayName = 'CommonList';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* TODO
|
|
|
|
*/
|
|
|
|
function getBadgeVariantFromLabel(
|
|
|
|
label: string
|
|
|
|
): ComponentProps<typeof Badge>['variant'] {
|
|
|
|
if (['work'].includes(label.toLowerCase())) {
|
|
|
|
return 'default';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (['personal'].includes(label.toLowerCase())) {
|
|
|
|
return 'outline';
|
|
|
|
}
|
|
|
|
|
|
|
|
return 'secondary';
|
|
|
|
}
|