tianji/src/client/components/CommonList.tsx

140 lines
3.9 KiB
TypeScript
Raw Normal View History

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';
import { LuSearch } from 'react-icons/lu';
import { Input } from './ui/input';
import { useFuseSearch } from '@/hooks/useFuseSearch';
import { Empty } from 'antd';
import { globalEventBus } from '@/utils/event';
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 {
isLoading?: boolean;
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();
const { searchText, setSearchText, searchResult } = useFuseSearch(
props.items,
{
keys: [
{
name: 'title',
weight: 1,
},
{
name: 'id',
weight: 0.6,
},
{
name: 'tags',
weight: 0.4,
},
],
}
);
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">
{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">
<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" />
<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">
{finalList.length === 0 && !props.isLoading && <Empty />}
{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',
isSelected && 'bg-muted'
)}
onClick={() => {
globalEventBus.emit('commonListSelected');
navigate({
to: item.href,
});
}}
>
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 ? (
<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';
}