13 changed files with 607 additions and 72 deletions
@ -0,0 +1,179 @@
@@ -0,0 +1,179 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { Checkbox } from '@/components/ui/checkbox' |
||||
import { Drawer, DrawerContent, DrawerHeader, DrawerTrigger } from '@/components/ui/drawer' |
||||
import { Label } from '@/components/ui/label' |
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' |
||||
import { DEFAULT_SHOW_KINDS, ExtendedKind } from '@/constants' |
||||
import { cn } from '@/lib/utils' |
||||
import { useKindFilter } from '@/providers/KindFilterProvider' |
||||
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||
import { ListFilter } from 'lucide-react' |
||||
import { kinds } from 'nostr-tools' |
||||
import { useEffect, useMemo, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
const SUPPORTED_KINDS = [ |
||||
{ kindGroup: [kinds.ShortTextNote, ExtendedKind.COMMENT], label: 'Posts' }, |
||||
{ kindGroup: [kinds.Repost], label: 'Reposts' }, |
||||
{ kindGroup: [kinds.LongFormArticle], label: 'Articles' }, |
||||
{ kindGroup: [kinds.Highlights], label: 'Highlights' }, |
||||
{ kindGroup: [ExtendedKind.POLL], label: 'Polls' }, |
||||
{ kindGroup: [ExtendedKind.VOICE, ExtendedKind.VOICE_COMMENT], label: 'Voice Posts' }, |
||||
{ kindGroup: [ExtendedKind.PICTURE], label: 'Photo Posts' } |
||||
] |
||||
|
||||
export default function KindFilter({ |
||||
showKinds, |
||||
onShowKindsChange |
||||
}: { |
||||
showKinds: number[] |
||||
onShowKindsChange: (kinds: number[]) => void |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { isSmallScreen } = useScreenSize() |
||||
const [open, setOpen] = useState(false) |
||||
const { updateShowKinds } = useKindFilter() |
||||
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) |
||||
const [isPersistent, setIsPersistent] = useState(false) |
||||
const isFilterApplied = useMemo(() => { |
||||
return showKinds.length !== DEFAULT_SHOW_KINDS.length |
||||
}, [showKinds]) |
||||
|
||||
useEffect(() => { |
||||
setTemporaryShowKinds(showKinds) |
||||
}, [open]) |
||||
|
||||
const handleApply = () => { |
||||
if (temporaryShowKinds.length === 0) { |
||||
// must select at least one kind
|
||||
return |
||||
} |
||||
|
||||
const newShowKinds = [...temporaryShowKinds].sort() |
||||
let isSame = true |
||||
for (let index = 0; index < newShowKinds.length; index++) { |
||||
if (showKinds[index] !== newShowKinds[index]) { |
||||
isSame = false |
||||
break |
||||
} |
||||
} |
||||
if (!isSame) { |
||||
onShowKindsChange(newShowKinds) |
||||
} |
||||
|
||||
if (isPersistent) { |
||||
updateShowKinds(newShowKinds) |
||||
} |
||||
|
||||
setIsPersistent(false) |
||||
setOpen(false) |
||||
} |
||||
|
||||
const trigger = ( |
||||
<Button |
||||
variant="ghost" |
||||
size="titlebar-icon" |
||||
className={cn('mr-1', !isFilterApplied && 'text-muted-foreground')} |
||||
onClick={() => { |
||||
if (isSmallScreen) { |
||||
setOpen(true) |
||||
} |
||||
}} |
||||
> |
||||
<ListFilter /> |
||||
</Button> |
||||
) |
||||
|
||||
const content = ( |
||||
<div> |
||||
<div className="grid grid-cols-2 gap-2"> |
||||
{SUPPORTED_KINDS.map(({ kindGroup, label }) => ( |
||||
<Label |
||||
key={label} |
||||
className="focus:bg-accent/50 cursor-pointer flex items-start gap-3 rounded-lg border px-4 py-3 has-[[aria-checked=true]]:border-primary has-[[aria-checked=true]]:bg-primary/20" |
||||
> |
||||
<Checkbox |
||||
id="toggle-2" |
||||
checked={kindGroup.every((k) => temporaryShowKinds.includes(k))} |
||||
onCheckedChange={(checked) => { |
||||
if (checked) { |
||||
// add all kinds in this group
|
||||
setTemporaryShowKinds((prev) => Array.from(new Set([...prev, ...kindGroup]))) |
||||
} else { |
||||
// remove all kinds in this group
|
||||
setTemporaryShowKinds((prev) => prev.filter((k) => !kindGroup.includes(k))) |
||||
} |
||||
}} |
||||
/> |
||||
<div className="grid gap-1.5"> |
||||
<p className="leading-none font-medium">{label}</p> |
||||
<p className="text-muted-foreground text-xs">kind {kindGroup.join(', ')}</p> |
||||
</div> |
||||
</Label> |
||||
))} |
||||
</div> |
||||
|
||||
<div className="flex gap-2 mt-4"> |
||||
<Button |
||||
variant="secondary" |
||||
onClick={() => { |
||||
setTemporaryShowKinds(DEFAULT_SHOW_KINDS) |
||||
}} |
||||
className="flex-1" |
||||
> |
||||
{t('Select All')} |
||||
</Button> |
||||
<Button |
||||
variant="secondary" |
||||
onClick={() => { |
||||
setTemporaryShowKinds([]) |
||||
}} |
||||
className="flex-1" |
||||
> |
||||
{t('Clear All')} |
||||
</Button> |
||||
</div> |
||||
|
||||
<Label className="flex items-center gap-2 cursor-pointer mt-4"> |
||||
<Checkbox |
||||
id="persistent-filter" |
||||
checked={isPersistent} |
||||
onCheckedChange={(checked) => setIsPersistent(!!checked)} |
||||
/> |
||||
<span className="text-sm">{t('Remember my choice')}</span> |
||||
</Label> |
||||
|
||||
<Button |
||||
onClick={handleApply} |
||||
className="mt-4 w-full" |
||||
disabled={temporaryShowKinds.length === 0} |
||||
> |
||||
{t('Apply')} |
||||
</Button> |
||||
</div> |
||||
) |
||||
|
||||
if (isSmallScreen) { |
||||
return ( |
||||
<> |
||||
{trigger} |
||||
<Drawer open={open} onOpenChange={setOpen}> |
||||
<DrawerTrigger asChild></DrawerTrigger> |
||||
<DrawerContent className="px-4"> |
||||
<DrawerHeader /> |
||||
{content} |
||||
</DrawerContent> |
||||
</Drawer> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
return ( |
||||
<Popover open={open} onOpenChange={setOpen}> |
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger> |
||||
<PopoverContent className="w-96" collisionPadding={16}> |
||||
{content} |
||||
</PopoverContent> |
||||
</Popover> |
||||
) |
||||
} |
||||
@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
|
||||
import * as React from 'react' |
||||
import * as CheckboxPrimitive from '@radix-ui/react-checkbox' |
||||
import { Check } from 'lucide-react' |
||||
|
||||
import { cn } from '@/lib/utils' |
||||
|
||||
const Checkbox = React.forwardRef< |
||||
React.ElementRef<typeof CheckboxPrimitive.Root>, |
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> |
||||
>(({ className, ...props }, ref) => ( |
||||
<CheckboxPrimitive.Root |
||||
ref={ref} |
||||
className={cn( |
||||
'peer h-4 w-4 shrink-0 rounded-sm border border-accent shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground', |
||||
className |
||||
)} |
||||
{...props} |
||||
> |
||||
<CheckboxPrimitive.Indicator className={cn('flex items-center justify-center text-current')}> |
||||
<Check className="h-4 w-4" /> |
||||
</CheckboxPrimitive.Indicator> |
||||
</CheckboxPrimitive.Root> |
||||
)) |
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName |
||||
|
||||
export { Checkbox } |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
import { createContext, useContext, useState } from 'react' |
||||
import storage from '@/services/local-storage.service' |
||||
|
||||
type TKindFilterContext = { |
||||
showKinds: number[] |
||||
updateShowKinds: (kinds: number[]) => void |
||||
} |
||||
|
||||
const KindFilterContext = createContext<TKindFilterContext | undefined>(undefined) |
||||
|
||||
export const useKindFilter = () => { |
||||
const context = useContext(KindFilterContext) |
||||
if (!context) { |
||||
throw new Error('useKindFilter must be used within a KindFilterProvider') |
||||
} |
||||
return context |
||||
} |
||||
|
||||
export function KindFilterProvider({ children }: { children: React.ReactNode }) { |
||||
const [showKinds, setShowKinds] = useState<number[]>(storage.getShowKinds()) |
||||
|
||||
const updateShowKinds = (kinds: number[]) => { |
||||
storage.setShowKinds(kinds) |
||||
setShowKinds(kinds) |
||||
} |
||||
|
||||
return ( |
||||
<KindFilterContext.Provider value={{ showKinds, updateShowKinds }}> |
||||
{children} |
||||
</KindFilterContext.Provider> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue