fix(link): improve UX, maintain state (#154)

* fix(topic): handleSelectLearningState missing depth

* fix(link): use active index instead of native focus

* chore(palette): use atom for maintain state

* chore(link): prevent keydown if command palette active

* fix: responsive link item

* chore: add active item index state to LinkRoute

* fix: ability to press enter go to edit mode
This commit is contained in:
Aslam
2024-09-08 13:38:23 +07:00
committed by GitHub
parent 1cd6063768
commit 7100ded35a
3 changed files with 101 additions and 79 deletions
+3 -3
View File
@@ -4,7 +4,7 @@ import { LinkHeader } from "@/components/routes/link/header"
import { LinkList } from "@/components/routes/link/list" import { LinkList } from "@/components/routes/link/list"
import { LinkManage } from "@/components/routes/link/manage" import { LinkManage } from "@/components/routes/link/manage"
import { useQueryState } from "nuqs" import { useQueryState } from "nuqs"
import { useEffect } from "react" import { useEffect, useState } from "react"
import { useAtom } from "jotai" import { useAtom } from "jotai"
import { linkEditIdAtom } from "@/store/link" import { linkEditIdAtom } from "@/store/link"
import { LinkBottomBar } from "./bottom-bar" import { LinkBottomBar } from "./bottom-bar"
@@ -12,6 +12,7 @@ import { LinkBottomBar } from "./bottom-bar"
export function LinkRoute() { export function LinkRoute() {
const [, setEditId] = useAtom(linkEditIdAtom) const [, setEditId] = useAtom(linkEditIdAtom)
const [nuqsEditId] = useQueryState("editId") const [nuqsEditId] = useQueryState("editId")
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null)
useEffect(() => { useEffect(() => {
setEditId(nuqsEditId) setEditId(nuqsEditId)
@@ -21,8 +22,7 @@ export function LinkRoute() {
<div className="flex h-full flex-auto flex-col overflow-hidden"> <div className="flex h-full flex-auto flex-col overflow-hidden">
<LinkHeader /> <LinkHeader />
<LinkManage /> <LinkManage />
{/* Refresh list everytime editId is changed */} <LinkList key={nuqsEditId} activeItemIndex={activeItemIndex} setActiveItemIndex={setActiveItemIndex} />
<LinkList key={nuqsEditId} />
<LinkBottomBar /> <LinkBottomBar />
</div> </div>
) )
+34 -6
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from "react" import React, { useCallback, useEffect, useMemo } from "react"
import { import {
DndContext, DndContext,
closestCenter, closestCenter,
@@ -22,13 +22,16 @@ import { useQueryState } from "nuqs"
import { learningStateAtom } from "./header" import { learningStateAtom } from "./header"
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette" import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
interface LinkListProps {} interface LinkListProps {
activeItemIndex: number | null
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
}
const LinkList: React.FC<LinkListProps> = () => { const LinkList: React.FC<LinkListProps> = ({ activeItemIndex, setActiveItemIndex }) => {
const [isCommandPalettePpen] = useAtom(commandPaletteOpenAtom) const [isCommandPalettePpen] = useAtom(commandPaletteOpenAtom)
const [editId, setEditId] = useQueryState("editId") const [editId, setEditId] = useQueryState("editId")
const [activeLearningState] = useAtom(learningStateAtom) const [activeLearningState] = useAtom(learningStateAtom)
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null) const [draggingId, setDraggingId] = React.useState<UniqueIdentifier | null>(null)
const { me } = useAccount({ const { me } = useAccount({
root: { personalLinks: [] } root: { personalLinks: [] }
@@ -36,7 +39,6 @@ const LinkList: React.FC<LinkListProps> = () => {
const personalLinks = useMemo(() => me?.root?.personalLinks || [], [me?.root?.personalLinks]) const personalLinks = useMemo(() => me?.root?.personalLinks || [], [me?.root?.personalLinks])
const [sort] = useAtom(linkSortAtom) const [sort] = useAtom(linkSortAtom)
const [draggingId, setDraggingId] = useState<UniqueIdentifier | null>(null)
const filteredLinks = useMemo( const filteredLinks = useMemo(
() => () =>
@@ -73,6 +75,16 @@ const LinkList: React.FC<LinkListProps> = () => {
} }
}) })
// on mounted, if editId is set, set activeItemIndex to the index of the item with the editId
useEffect(() => {
if (editId) {
const index = sortedLinks.findIndex(link => link?.id === editId)
if (index !== -1) {
setActiveItemIndex(index)
}
}
}, [editId, sortedLinks, setActiveItemIndex])
const updateSequences = useCallback((links: PersonalLinkLists) => { const updateSequences = useCallback((links: PersonalLinkLists) => {
links.forEach((link, index) => { links.forEach((link, index) => {
if (link) { if (link) {
@@ -111,12 +123,28 @@ const LinkList: React.FC<LinkListProps> = () => {
return newIndex return newIndex
}) })
} else if (e.key === "Enter" && activeItemIndex !== null) {
e.preventDefault()
const activeLink = sortedLinks[activeItemIndex]
if (activeLink) {
setEditId(activeLink.id)
}
} }
} }
window.addEventListener("keydown", handleKeyDown) window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown) return () => window.removeEventListener("keydown", handleKeyDown)
}, [me?.root?.personalLinks, sortedLinks, editId, sort, updateSequences, isCommandPalettePpen]) }, [
me?.root?.personalLinks,
sortedLinks,
editId,
sort,
updateSequences,
isCommandPalettePpen,
activeItemIndex,
setEditId,
setActiveItemIndex
])
const handleDragStart = useCallback( const handleDragStart = useCallback(
(event: DragStartEvent) => { (event: DragStartEvent) => {
@@ -90,14 +90,16 @@ export const LinkItem: React.FC<LinkItemProps> = ({
onFocus={() => setActiveItemIndex(index)} onFocus={() => setActiveItemIndex(index)}
onBlur={() => setActiveItemIndex(null)} onBlur={() => setActiveItemIndex(null)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={cn("relative flex h-14 cursor-default items-center outline-none xl:h-11", { className={cn(
"relative cursor-default outline-none",
"grid grid-cols-[auto_1fr_auto] items-center gap-x-2 px-2 py-2 sm:px-4 sm:py-2",
{
"bg-muted-foreground/10": isActive, "bg-muted-foreground/10": isActive,
"hover:bg-muted/50": !isActive "hover:bg-muted/50": !isActive
})} }
)}
onDoubleClick={handleRowDoubleClick} onDoubleClick={handleRowDoubleClick}
> >
<div className="flex grow justify-between gap-x-6 px-6 max-lg:px-4">
<div className="flex min-w-0 items-center gap-x-4">
<Popover <Popover
open={openPopoverForId === personalLink.id} open={openPopoverForId === personalLink.id}
onOpenChange={(open: boolean) => setOpenPopoverForId(open ? personalLink.id : null)} onOpenChange={(open: boolean) => setOpenPopoverForId(open ? personalLink.id : null)}
@@ -126,47 +128,39 @@ export const LinkItem: React.FC<LinkItemProps> = ({
</PopoverContent> </PopoverContent>
</Popover> </Popover>
<div className="flex min-w-0 flex-col items-start gap-y-1 overflow-hidden md:flex-row md:items-center md:gap-x-2">
{personalLink.icon && ( {personalLink.icon && (
<Image <Image
src={personalLink.icon} src={personalLink.icon}
alt={personalLink.title} alt={personalLink.title}
className="size-5 rounded-full" className="size-5 shrink-0 rounded-full"
width={16} width={16}
height={16} height={16}
/> />
)} )}
<div className="w-full min-w-0 flex-auto"> <div className="flex min-w-0 flex-col items-start gap-y-1 overflow-hidden md:flex-row md:items-center md:gap-x-2">
<div className="gap-x-2 space-y-0.5 xl:flex xl:flex-row"> <p className="text-primary hover:text-primary truncate text-sm font-medium">{personalLink.title}</p>
<p className="text-primary hover:text-primary line-clamp-1 text-sm font-medium xl:truncate">
{personalLink.title}
</p>
{personalLink.url && ( {personalLink.url && (
<div className="group flex items-center gap-x-1"> <div className="text-muted-foreground flex min-w-0 shrink items-center gap-x-1">
<LaIcon <LaIcon name="Link" aria-hidden="true" className="size-3 flex-none" />
name="Link"
aria-hidden="true"
className="text-muted-foreground group-hover:text-primary flex-none"
/>
<Link <Link
href={personalLink.url} href={personalLink.url}
passHref passHref
prefetch={false} prefetch={false}
target="_blank" target="_blank"
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
className="text-muted-foreground hover:text-primary text-xs" className="hover:text-primary truncate text-xs"
> >
<span className="xl:truncate">{personalLink.url}</span> {personalLink.url}
</Link> </Link>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div>
<div className="flex shrink-0 items-center gap-x-4"> <div className="flex shrink-0 items-center justify-end">
{personalLink.topic && <Badge variant="secondary">{personalLink.topic.prettyName}</Badge>} {personalLink.topic && <Badge variant="secondary">{personalLink.topic.prettyName}</Badge>}
</div> </div>
</div>
</li> </li>
) )
} }