mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
* chore: expose scrollActiveElementIntoView * feat(utils): editable element * fix: memoize exceptionRefs, use animation frame and check editable element * fix: improve btn on mobile * chore(drps): bump framer motion version * fix(link): big fix * chore: remove comment code * feat: touch device
158 lines
4.8 KiB
TypeScript
158 lines
4.8 KiB
TypeScript
import React, { useCallback, useEffect, useMemo } from "react"
|
|
import { Primitive } from "@radix-ui/react-primitive"
|
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
|
import { atom, useAtom } from "jotai"
|
|
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
|
import { TopicItem } from "./partials/topic-item"
|
|
import { useMedia } from "react-use"
|
|
import { useRouter } from "next/navigation"
|
|
import { useActiveItemScroll } from "@/hooks/use-active-item-scroll"
|
|
import { Column } from "@/components/custom/column"
|
|
import { useColumnStyles } from "./hooks/use-column-styles"
|
|
import { LaAccount, ListOfTopics, Topic, UserRoot } from "@/lib/schema"
|
|
import { LearningStateValue } from "@/lib/constants"
|
|
|
|
interface TopicListProps {
|
|
activeItemIndex: number | null
|
|
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
|
|
disableEnterKey: boolean
|
|
}
|
|
|
|
interface MainTopicListProps extends TopicListProps {
|
|
me: {
|
|
root: {
|
|
topicsWantToLearn: ListOfTopics
|
|
topicsLearning: ListOfTopics
|
|
topicsLearned: ListOfTopics
|
|
} & UserRoot
|
|
} & LaAccount
|
|
}
|
|
|
|
export interface PersonalTopic {
|
|
topic: Topic | null
|
|
learningState: LearningStateValue
|
|
}
|
|
|
|
export const topicOpenPopoverForIdAtom = atom<string | null>(null)
|
|
|
|
export const TopicList: React.FC<TopicListProps> = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => {
|
|
const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } })
|
|
|
|
if (!me) return null
|
|
|
|
return (
|
|
<MainTopicList
|
|
me={me}
|
|
activeItemIndex={activeItemIndex}
|
|
setActiveItemIndex={setActiveItemIndex}
|
|
disableEnterKey={disableEnterKey}
|
|
/>
|
|
)
|
|
}
|
|
|
|
export const MainTopicList: React.FC<MainTopicListProps> = ({
|
|
me,
|
|
activeItemIndex,
|
|
setActiveItemIndex,
|
|
disableEnterKey
|
|
}) => {
|
|
const isTablet = useMedia("(max-width: 640px)")
|
|
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
|
|
const router = useRouter()
|
|
|
|
const personalTopics = useMemo(
|
|
() => [
|
|
...me.root.topicsWantToLearn.map(topic => ({ topic, learningState: "wantToLearn" as const })),
|
|
...me.root.topicsLearning.map(topic => ({ topic, learningState: "learning" as const })),
|
|
...me.root.topicsLearned.map(topic => ({ topic, learningState: "learned" as const }))
|
|
],
|
|
[me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned]
|
|
)
|
|
|
|
const itemCount = personalTopics.length
|
|
|
|
const handleEnter = useCallback(
|
|
(selectedTopic: Topic) => {
|
|
router.push(`/${selectedTopic.name}`)
|
|
},
|
|
[router]
|
|
)
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (isCommandPaletteOpen) return
|
|
|
|
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
|
e.preventDefault()
|
|
setActiveItemIndex(prevIndex => {
|
|
if (prevIndex === null) return 0
|
|
const newIndex = e.key === "ArrowUp" ? (prevIndex - 1 + itemCount) % itemCount : (prevIndex + 1) % itemCount
|
|
return newIndex
|
|
})
|
|
} else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null && personalTopics) {
|
|
e.preventDefault()
|
|
const selectedTopic = personalTopics[activeItemIndex]
|
|
if (selectedTopic?.topic) handleEnter?.(selectedTopic.topic)
|
|
}
|
|
},
|
|
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalTopics, handleEnter]
|
|
)
|
|
|
|
useEffect(() => {
|
|
window.addEventListener("keydown", handleKeyDown)
|
|
return () => window.removeEventListener("keydown", handleKeyDown)
|
|
}, [handleKeyDown])
|
|
|
|
return (
|
|
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
|
{!isTablet && <ColumnHeader />}
|
|
<TopicListItems personalTopics={personalTopics} activeItemIndex={activeItemIndex} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export const ColumnHeader: React.FC = () => {
|
|
const columnStyles = useColumnStyles()
|
|
|
|
return (
|
|
<div className="flex h-8 shrink-0 grow-0 flex-row gap-4 border-b max-lg:px-4 sm:px-6">
|
|
<Column.Wrapper style={columnStyles.title}>
|
|
<Column.Text>Name</Column.Text>
|
|
</Column.Wrapper>
|
|
<Column.Wrapper style={columnStyles.topic}>
|
|
<Column.Text>State</Column.Text>
|
|
</Column.Wrapper>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
interface TopicListItemsProps {
|
|
personalTopics: PersonalTopic[] | null
|
|
activeItemIndex: number | null
|
|
}
|
|
|
|
const TopicListItems: React.FC<TopicListItemsProps> = ({ personalTopics, activeItemIndex }) => {
|
|
const { setElementRef } = useActiveItemScroll<HTMLDivElement>({ activeIndex: activeItemIndex })
|
|
|
|
return (
|
|
<Primitive.div
|
|
className="divide-primary/5 flex flex-1 flex-col divide-y overflow-y-auto outline-none [scrollbar-gutter:stable]"
|
|
tabIndex={-1}
|
|
role="list"
|
|
>
|
|
{personalTopics?.map(
|
|
(pt, index) =>
|
|
pt.topic?.id && (
|
|
<TopicItem
|
|
key={pt.topic.id}
|
|
ref={el => setElementRef(el, index)}
|
|
topic={pt.topic}
|
|
learningState={pt.learningState}
|
|
isActive={index === activeItemIndex}
|
|
/>
|
|
)
|
|
)}
|
|
</Primitive.div>
|
|
)
|
|
}
|