mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix: conflict
This commit is contained in:
@@ -8,7 +8,7 @@ import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
|||||||
import { linkLearningStateSelectorAtom } from "@/store/link"
|
import { linkLearningStateSelectorAtom } from "@/store/link"
|
||||||
import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command"
|
import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command"
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||||
import type { icons } from "lucide-react"
|
import { icons } from "lucide-react"
|
||||||
|
|
||||||
interface LearningStateSelectorProps {
|
interface LearningStateSelectorProps {
|
||||||
showSearch?: boolean
|
showSearch?: boolean
|
||||||
@@ -37,6 +37,9 @@ export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
|
|||||||
setIsLearningStateSelectorOpen(false)
|
setIsLearningStateSelectorOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iconName = selectedLearningState?.icon || defaultIcon
|
||||||
|
const labelText = selectedLearningState?.label || defaultLabel
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={isLearningStateSelectorOpen} onOpenChange={setIsLearningStateSelectorOpen}>
|
<Popover open={isLearningStateSelectorOpen} onOpenChange={setIsLearningStateSelectorOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
@@ -47,20 +50,8 @@ export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className={cn("gap-x-2 text-sm", className)}
|
className={cn("gap-x-2 text-sm", className)}
|
||||||
>
|
>
|
||||||
{selectedLearningState?.icon ||
|
{iconName && <LaIcon name={iconName} className={cn(selectedLearningState?.className)} />}
|
||||||
(defaultIcon && (
|
{labelText && <span className={cn("truncate", selectedLearningState?.className || "")}>{labelText}</span>}
|
||||||
<LaIcon
|
|
||||||
name={selectedLearningState?.icon || defaultIcon}
|
|
||||||
className={cn(selectedLearningState?.className)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{selectedLearningState?.label ||
|
|
||||||
(defaultLabel && (
|
|
||||||
<span className={cn("truncate", selectedLearningState?.className || "")}>
|
|
||||||
{selectedLearningState?.label || defaultLabel}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<LaIcon name="ChevronDown" />
|
<LaIcon name="ChevronDown" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -97,7 +88,7 @@ export const LearningStateSelectorContent: React.FC<LearningStateSelectorContent
|
|||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{LEARNING_STATES.map(ls => (
|
{LEARNING_STATES.map(ls => (
|
||||||
<CommandItem key={ls.value} value={ls.value} onSelect={onSelect}>
|
<CommandItem key={ls.value} value={ls.value} onSelect={onSelect}>
|
||||||
<LaIcon name={ls.icon} className={cn("mr-2", ls.className)} />
|
{ls.icon && <LaIcon name={ls.icon} className={cn("mr-2", ls.className)} />}
|
||||||
<span className={ls.className}>{ls.label}</span>
|
<span className={ls.className}>{ls.label}</span>
|
||||||
<LaIcon
|
<LaIcon
|
||||||
name="Check"
|
name="Check"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const LinkSectionHeader: React.FC<LinkSectionHeaderProps> = ({ linkCount }) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[30px] items-center gap-px rounded-md",
|
"flex h-9 items-center gap-px rounded-md sm:h-[30px]",
|
||||||
isLinksActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
isLinksActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -55,7 +55,7 @@ const LinkSectionHeader: React.FC<LinkSectionHeaderProps> = ({ linkCount }) => {
|
|||||||
href="/links"
|
href="/links"
|
||||||
className="flex flex-1 items-center justify-start rounded-md px-2 py-1 focus-visible:outline-none focus-visible:ring-0"
|
className="flex flex-1 items-center justify-start rounded-md px-2 py-1 focus-visible:outline-none focus-visible:ring-0"
|
||||||
>
|
>
|
||||||
<p className="flex w-full items-center text-xs font-medium">
|
<p className="flex w-full items-center text-sm font-medium sm:text-xs">
|
||||||
Links
|
Links
|
||||||
{linkCount > 0 && <span className="text-muted-foreground ml-1">{linkCount}</span>}
|
{linkCount > 0 && <span className="text-muted-foreground ml-1">{linkCount}</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -112,7 +112,7 @@ const ListItem: React.FC<ListItemProps> = ({ label, href, count, isActive }) =>
|
|||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium",
|
"relative flex h-9 w-full items-center gap-2 rounded-md p-1.5 font-medium sm:h-8",
|
||||||
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -77,12 +77,12 @@ interface PageSectionHeaderProps {
|
|||||||
const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({ pageCount, isActive }) => (
|
const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({ pageCount, isActive }) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[30px] items-center gap-px rounded-md",
|
"flex h-9 items-center gap-px rounded-md sm:h-[30px]",
|
||||||
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href="/pages" className="flex flex-1 items-center justify-start rounded-md px-2 py-1">
|
<Link href="/pages" className="flex flex-1 items-center justify-start rounded-md px-2 py-1">
|
||||||
<p className="text-xs">
|
<p className="text-sm sm:text-xs">
|
||||||
Pages
|
Pages
|
||||||
{pageCount > 0 && <span className="text-muted-foreground ml-1">{pageCount}</span>}
|
{pageCount > 0 && <span className="text-muted-foreground ml-1">{pageCount}</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -165,7 +165,7 @@ const PageListItem: React.FC<PageListItemProps> = ({ page, isActive }) => (
|
|||||||
<Link
|
<Link
|
||||||
href={`/pages/${page.id}`}
|
href={`/pages/${page.id}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group-hover/sidebar-link:bg-accent group-hover/sidebar-link:text-accent-foreground relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium",
|
"group-hover/sidebar-link:bg-accent group-hover/sidebar-link:text-accent-foreground relative flex h-9 w-full items-center gap-2 rounded-md p-1.5 font-medium sm:h-8",
|
||||||
{ "bg-accent text-accent-foreground": isActive }
|
{ "bg-accent text-accent-foreground": isActive }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Link from "next/link"
|
|||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import { LaIcon } from "@/components/custom/la-icon"
|
import { LaIcon } from "@/components/custom/la-icon"
|
||||||
import { ListOfTopics } from "@/lib/schema"
|
import { ListOfTopics } from "@/lib/schema"
|
||||||
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
||||||
@@ -46,12 +45,12 @@ interface TopicSectionHeaderProps {
|
|||||||
const TopicSectionHeader: React.FC<TopicSectionHeaderProps> = ({ topicCount, isActive }) => (
|
const TopicSectionHeader: React.FC<TopicSectionHeaderProps> = ({ topicCount, isActive }) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex min-h-[30px] items-center gap-px rounded-md",
|
"flex h-9 items-center gap-px rounded-md sm:h-[30px]",
|
||||||
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link href="/topics" className="flex flex-1 items-center justify-start rounded-md px-2 py-1">
|
<Link href="/topics" className="flex flex-1 items-center justify-start rounded-md px-2 py-1">
|
||||||
<p className="text-xs">
|
<p className="text-sm sm:text-xs">
|
||||||
Topics
|
Topics
|
||||||
{topicCount > 0 && <span className="text-muted-foreground ml-1">{topicCount}</span>}
|
{topicCount > 0 && <span className="text-muted-foreground ml-1">{topicCount}</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -115,9 +114,9 @@ const ListItem: React.FC<ListItemProps> = ({ label, value, href, count, isActive
|
|||||||
<div className="group/reorder-page relative">
|
<div className="group/reorder-page relative">
|
||||||
<div className="group/topic-link relative flex min-w-0 flex-1">
|
<div className="group/topic-link relative flex min-w-0 flex-1">
|
||||||
<Link
|
<Link
|
||||||
href={"#"}
|
href={href}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group-hover/topic-link:bg-accent relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium",
|
"group-hover/topic-link:bg-accent relative flex h-9 w-full items-center gap-2 rounded-md p-1.5 font-medium sm:h-8",
|
||||||
{ "bg-accent text-accent-foreground": isActive },
|
{ "bg-accent text-accent-foreground": isActive },
|
||||||
le.className
|
le.className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const LinkHeader = React.memo(() => {
|
|||||||
</ContentHeader>
|
</ContentHeader>
|
||||||
|
|
||||||
{isTablet && (
|
{isTablet && (
|
||||||
<div className="flex min-h-10 flex-row items-start justify-between border-b px-6 pb-4 pt-2 max-lg:pl-4">
|
<div className="flex flex-row items-start justify-between border-b px-6 pb-4 pt-2 max-lg:pl-4">
|
||||||
<LearningTab />
|
<LearningTab />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -87,7 +87,10 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
|
|||||||
aria-selected={isActive}
|
aria-selected={isActive}
|
||||||
data-disabled={disabled}
|
data-disabled={disabled}
|
||||||
data-active={isActive}
|
data-active={isActive}
|
||||||
className="w-full overflow-visible border-b-[0.5px] border-transparent outline-none data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
className={cn(
|
||||||
|
"w-full overflow-visible border-b-[0.5px] border-transparent outline-none",
|
||||||
|
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
||||||
|
)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -149,7 +152,7 @@ export const LinkItem = React.forwardRef<HTMLDivElement, LinkItemProps>(
|
|||||||
prefetch={false}
|
prefetch={false}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
className="hover:text-primary truncate text-xs"
|
className="hover:text-primary mr-1 truncate text-xs"
|
||||||
>
|
>
|
||||||
{personalLink.url}
|
{personalLink.url}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,35 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
|
||||||
import { PageHeader } from "./header"
|
import { PageHeader } from "./header"
|
||||||
import { PageList } from "./list"
|
import { PageList } from "./list"
|
||||||
import { useAtom } from "jotai"
|
|
||||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
|
||||||
|
|
||||||
export function PageRoute() {
|
export function PageRoute() {
|
||||||
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null)
|
|
||||||
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
|
|
||||||
const [disableEnterKey, setDisableEnterKey] = useState(false)
|
|
||||||
|
|
||||||
const handleCommandPaletteClose = useCallback(() => {
|
|
||||||
setDisableEnterKey(true)
|
|
||||||
setTimeout(() => setDisableEnterKey(false), 100)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isCommandPaletteOpen) {
|
|
||||||
handleCommandPaletteClose()
|
|
||||||
}
|
|
||||||
}, [isCommandPaletteOpen, handleCommandPaletteClose])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-auto flex-col overflow-hidden">
|
<div className="flex h-full flex-auto flex-col overflow-hidden">
|
||||||
<PageHeader />
|
<PageHeader />
|
||||||
<PageList
|
<PageList />
|
||||||
activeItemIndex={activeItemIndex}
|
|
||||||
setActiveItemIndex={setActiveItemIndex}
|
|
||||||
disableEnterKey={disableEnterKey}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,33 +23,6 @@ import { usePageActions } from "../hooks/use-page-actions"
|
|||||||
|
|
||||||
const TITLE_PLACEHOLDER = "Untitled"
|
const TITLE_PLACEHOLDER = "Untitled"
|
||||||
|
|
||||||
const isPageEmpty = (page: PersonalPage): boolean => {
|
|
||||||
return (!page.title || page.title.trim() === "") && (!page.content || Object.keys(page.content).length === 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useDeleteEmptyPage = (currentPageId: string | null) => {
|
|
||||||
const router = useRouter()
|
|
||||||
const { me } = useAccount({
|
|
||||||
root: {
|
|
||||||
personalPages: []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (!currentPageId || !me?.root?.personalPages) return
|
|
||||||
|
|
||||||
const currentPage = me.root.personalPages.find(page => page?.id === currentPageId)
|
|
||||||
if (currentPage && isPageEmpty(currentPage)) {
|
|
||||||
const index = me.root.personalPages.findIndex(page => page?.id === currentPageId)
|
|
||||||
if (index !== -1) {
|
|
||||||
me.root.personalPages.splice(index, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [currentPageId, me, router])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PageDetailRoute({ pageId }: { pageId: string }) {
|
export function PageDetailRoute({ pageId }: { pageId: string }) {
|
||||||
const { me } = useAccount({ root: { personalLinks: [] } })
|
const { me } = useAccount({ root: { personalLinks: [] } })
|
||||||
const isMobile = useMedia("(max-width: 770px)")
|
const isMobile = useMedia("(max-width: 770px)")
|
||||||
@@ -58,8 +31,6 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
|||||||
const { deletePage } = usePageActions()
|
const { deletePage } = usePageActions()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
|
|
||||||
// useDeleteEmptyPage(pageId)
|
|
||||||
|
|
||||||
const handleDelete = useCallback(async () => {
|
const handleDelete = useCallback(async () => {
|
||||||
const result = await confirm({
|
const result = await confirm({
|
||||||
title: "Delete page",
|
title: "Delete page",
|
||||||
|
|||||||
@@ -1,66 +1,71 @@
|
|||||||
import React, { useMemo, useCallback, useEffect } from "react"
|
import * as React from "react"
|
||||||
import { Primitive } from "@radix-ui/react-primitive"
|
import { Primitive } from "@radix-ui/react-primitive"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
import { useAtom } from "jotai"
|
|
||||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
|
||||||
import { PageItem } from "./partials/page-item"
|
import { PageItem } from "./partials/page-item"
|
||||||
import { useMedia } from "@/hooks/use-media"
|
import { useMedia } from "@/hooks/use-media"
|
||||||
import { useColumnStyles } from "./hooks/use-column-styles"
|
import { useColumnStyles } from "./hooks/use-column-styles"
|
||||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema"
|
|
||||||
import { useRouter } from "next/navigation"
|
|
||||||
import { useActiveItemScroll } from "@/hooks/use-active-item-scroll"
|
import { useActiveItemScroll } from "@/hooks/use-active-item-scroll"
|
||||||
import { Column } from "@/components/custom/column"
|
import { Column } from "@/components/custom/column"
|
||||||
|
import { useKeyDown } from "@/hooks/use-key-down"
|
||||||
|
|
||||||
interface PageListProps {
|
interface PageListProps {}
|
||||||
activeItemIndex: number | null
|
|
||||||
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
|
|
||||||
disableEnterKey: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => {
|
export const PageList: React.FC<PageListProps> = () => {
|
||||||
const isTablet = useMedia("(max-width: 640px)")
|
const isTablet = useMedia("(max-width: 640px)")
|
||||||
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
|
|
||||||
const { me } = useAccount({ root: { personalPages: [] } })
|
const { me } = useAccount({ root: { personalPages: [] } })
|
||||||
const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(null)
|
||||||
const router = useRouter()
|
const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
|
||||||
const itemCount = personalPages?.length || 0
|
const personalPages = React.useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
||||||
|
|
||||||
const handleEnter = useCallback(
|
const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalPages?.length ?? 0) - 1)
|
||||||
(selectedPage: PersonalPage) => {
|
|
||||||
router.push(`/pages/${selectedPage.id}`)
|
|
||||||
},
|
|
||||||
[router]
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
if (isCommandPaletteOpen) return
|
|
||||||
|
|
||||||
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||||
e.preventDefault()
|
switch (ev.key) {
|
||||||
setActiveItemIndex(prevIndex => {
|
case "ArrowDown":
|
||||||
if (prevIndex === null) return 0
|
ev.preventDefault()
|
||||||
const newIndex = e.key === "ArrowUp" ? (prevIndex - 1 + itemCount) % itemCount : (prevIndex + 1) % itemCount
|
ev.stopPropagation()
|
||||||
return newIndex
|
setActiveItemIndex(next())
|
||||||
})
|
setKeyboardActiveIndex(next())
|
||||||
} else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null && personalPages) {
|
break
|
||||||
e.preventDefault()
|
case "ArrowUp":
|
||||||
const selectedPage = personalPages[activeItemIndex]
|
ev.preventDefault()
|
||||||
if (selectedPage) handleEnter?.(selectedPage)
|
ev.stopPropagation()
|
||||||
}
|
setActiveItemIndex(prev())
|
||||||
},
|
setKeyboardActiveIndex(prev())
|
||||||
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, handleEnter]
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useKeyDown(() => true, handleKeyDown)
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
const { setElementRef } = useActiveItemScroll<HTMLAnchorElement>({ activeIndex: keyboardActiveIndex })
|
||||||
}, [handleKeyDown])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
||||||
{!isTablet && <ColumnHeader />}
|
{!isTablet && <ColumnHeader />}
|
||||||
<PageListItems personalPages={personalPages} activeItemIndex={activeItemIndex} />
|
<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"
|
||||||
|
>
|
||||||
|
{personalPages?.map(
|
||||||
|
(page, index) =>
|
||||||
|
page?.id && (
|
||||||
|
<PageItem
|
||||||
|
key={page.id}
|
||||||
|
ref={el => setElementRef(el, index)}
|
||||||
|
page={page}
|
||||||
|
isActive={index === activeItemIndex}
|
||||||
|
onPointerMove={() => {
|
||||||
|
setKeyboardActiveIndex(null)
|
||||||
|
setActiveItemIndex(index)
|
||||||
|
}}
|
||||||
|
data-keyboard-active={keyboardActiveIndex === index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Primitive.div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -82,32 +87,3 @@ export const ColumnHeader: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageListItemsProps {
|
|
||||||
personalPages?: PersonalPageLists | null
|
|
||||||
activeItemIndex: number | null
|
|
||||||
}
|
|
||||||
|
|
||||||
const PageListItems: React.FC<PageListItemsProps> = ({ personalPages, activeItemIndex }) => {
|
|
||||||
const { setElementRef } = useActiveItemScroll<HTMLAnchorElement>({ 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"
|
|
||||||
>
|
|
||||||
{personalPages?.map(
|
|
||||||
(page, index) =>
|
|
||||||
page?.id && (
|
|
||||||
<PageItem
|
|
||||||
key={page.id}
|
|
||||||
ref={el => setElementRef(el, index)}
|
|
||||||
page={page}
|
|
||||||
isActive={index === activeItemIndex}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Primitive.div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,26 +7,43 @@ import { useMedia } from "@/hooks/use-media"
|
|||||||
import { useColumnStyles } from "../hooks/use-column-styles"
|
import { useColumnStyles } from "../hooks/use-column-styles"
|
||||||
import { format } from "date-fns"
|
import { format } from "date-fns"
|
||||||
import { Column } from "@/components/custom/column"
|
import { Column } from "@/components/custom/column"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
interface PageItemProps {
|
interface PageItemProps extends React.HTMLAttributes<HTMLAnchorElement> {
|
||||||
page: PersonalPage
|
page: PersonalPage
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ page, isActive }, ref) => {
|
export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ page, isActive, ...props }, ref) => {
|
||||||
const isTablet = useMedia("(max-width: 640px)")
|
const isTablet = useMedia("(max-width: 640px)")
|
||||||
const columnStyles = useColumnStyles()
|
const columnStyles = useColumnStyles()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleKeyDown = React.useCallback(
|
||||||
|
(ev: React.KeyboardEvent<HTMLAnchorElement>) => {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
router.push(`/pages/${page.id}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[router, page.id]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
ref={ref}
|
ref={ref}
|
||||||
tabIndex={isActive ? 0 : -1}
|
tabIndex={isActive ? 0 : -1}
|
||||||
className={cn("relative block cursor-default outline-none", "min-h-12 py-2 max-lg:px-4 sm:px-6", {
|
className={cn(
|
||||||
"bg-muted-foreground/5": isActive,
|
"relative block cursor-default outline-none",
|
||||||
"hover:bg-muted/50": !isActive
|
"min-h-12 py-2 max-lg:px-4 sm:px-6",
|
||||||
})}
|
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
||||||
|
)}
|
||||||
href={`/pages/${page.id}`}
|
href={`/pages/${page.id}`}
|
||||||
role="listitem"
|
aria-selected={isActive}
|
||||||
|
data-active={isActive}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex h-full items-center gap-4">
|
<div className="flex h-full items-center gap-4">
|
||||||
<Column.Wrapper style={columnStyles.title}>
|
<Column.Wrapper style={columnStyles.title}>
|
||||||
|
|||||||
@@ -1,35 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useState } from "react"
|
|
||||||
import { TopicHeader } from "./header"
|
import { TopicHeader } from "./header"
|
||||||
import { TopicList } from "./list"
|
import { TopicList } from "./list"
|
||||||
import { useAtom } from "jotai"
|
|
||||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
|
||||||
|
|
||||||
export function TopicRoute() {
|
export function TopicRoute() {
|
||||||
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null)
|
|
||||||
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
|
|
||||||
const [disableEnterKey, setDisableEnterKey] = useState(false)
|
|
||||||
|
|
||||||
const handleCommandPaletteClose = useCallback(() => {
|
|
||||||
setDisableEnterKey(true)
|
|
||||||
setTimeout(() => setDisableEnterKey(false), 100)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isCommandPaletteOpen) {
|
|
||||||
handleCommandPaletteClose()
|
|
||||||
}
|
|
||||||
}, [isCommandPaletteOpen, handleCommandPaletteClose])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-auto flex-col overflow-hidden">
|
<div className="flex h-full flex-auto flex-col overflow-hidden">
|
||||||
<TopicHeader />
|
<TopicHeader />
|
||||||
<TopicList
|
<TopicList />
|
||||||
activeItemIndex={activeItemIndex}
|
|
||||||
setActiveItemIndex={setActiveItemIndex}
|
|
||||||
disableEnterKey={disableEnterKey}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import React, { useCallback, useEffect, useMemo } from "react"
|
import * as React from "react"
|
||||||
import { Primitive } from "@radix-ui/react-primitive"
|
import { Primitive } from "@radix-ui/react-primitive"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
import { atom, useAtom } from "jotai"
|
import { atom } from "jotai"
|
||||||
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
|
|
||||||
import { TopicItem } from "./partials/topic-item"
|
import { TopicItem } from "./partials/topic-item"
|
||||||
import { useMedia } from "@/hooks/use-media"
|
import { useMedia } from "@/hooks/use-media"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
@@ -11,12 +10,9 @@ import { Column } from "@/components/custom/column"
|
|||||||
import { useColumnStyles } from "./hooks/use-column-styles"
|
import { useColumnStyles } from "./hooks/use-column-styles"
|
||||||
import { LaAccount, ListOfTopics, Topic, UserRoot } from "@/lib/schema"
|
import { LaAccount, ListOfTopics, Topic, UserRoot } from "@/lib/schema"
|
||||||
import { LearningStateValue } from "@/lib/constants"
|
import { LearningStateValue } from "@/lib/constants"
|
||||||
|
import { useKeyDown } from "@/hooks/use-key-down"
|
||||||
|
|
||||||
interface TopicListProps {
|
interface TopicListProps {}
|
||||||
activeItemIndex: number | null
|
|
||||||
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
|
|
||||||
disableEnterKey: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MainTopicListProps extends TopicListProps {
|
interface MainTopicListProps extends TopicListProps {
|
||||||
me: {
|
me: {
|
||||||
@@ -35,32 +31,21 @@ export interface PersonalTopic {
|
|||||||
|
|
||||||
export const topicOpenPopoverForIdAtom = atom<string | null>(null)
|
export const topicOpenPopoverForIdAtom = atom<string | null>(null)
|
||||||
|
|
||||||
export const TopicList: React.FC<TopicListProps> = ({ activeItemIndex, setActiveItemIndex, disableEnterKey }) => {
|
export const TopicList: React.FC<TopicListProps> = () => {
|
||||||
const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } })
|
const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } })
|
||||||
|
|
||||||
if (!me) return null
|
if (!me) return null
|
||||||
|
|
||||||
return (
|
return <MainTopicList me={me} />
|
||||||
<MainTopicList
|
|
||||||
me={me}
|
|
||||||
activeItemIndex={activeItemIndex}
|
|
||||||
setActiveItemIndex={setActiveItemIndex}
|
|
||||||
disableEnterKey={disableEnterKey}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MainTopicList: React.FC<MainTopicListProps> = ({
|
export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
|
||||||
me,
|
|
||||||
activeItemIndex,
|
|
||||||
setActiveItemIndex,
|
|
||||||
disableEnterKey
|
|
||||||
}) => {
|
|
||||||
const isTablet = useMedia("(max-width: 640px)")
|
const isTablet = useMedia("(max-width: 640px)")
|
||||||
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
|
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(null)
|
||||||
|
const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const personalTopics = useMemo(
|
const personalTopics = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
...me.root.topicsWantToLearn.map(topic => ({ topic, learningState: "wantToLearn" as const })),
|
...me.root.topicsWantToLearn.map(topic => ({ topic, learningState: "wantToLearn" as const })),
|
||||||
...me.root.topicsLearning.map(topic => ({ topic, learningState: "learning" as const })),
|
...me.root.topicsLearning.map(topic => ({ topic, learningState: "learning" as const })),
|
||||||
@@ -69,44 +54,63 @@ export const MainTopicList: React.FC<MainTopicListProps> = ({
|
|||||||
[me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned]
|
[me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned]
|
||||||
)
|
)
|
||||||
|
|
||||||
const itemCount = personalTopics.length
|
const handleEnter = React.useCallback(
|
||||||
|
|
||||||
const handleEnter = useCallback(
|
|
||||||
(selectedTopic: Topic) => {
|
(selectedTopic: Topic) => {
|
||||||
router.push(`/${selectedTopic.name}`)
|
router.push(`/${selectedTopic.name}`)
|
||||||
},
|
},
|
||||||
[router]
|
[router]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalTopics?.length ?? 0) - 1)
|
||||||
(e: KeyboardEvent) => {
|
|
||||||
if (isCommandPaletteOpen) return
|
|
||||||
|
|
||||||
if (e.key === "ArrowUp" || e.key === "ArrowDown") {
|
const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
|
||||||
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(() => {
|
const handleKeyDown = (ev: KeyboardEvent) => {
|
||||||
window.addEventListener("keydown", handleKeyDown)
|
switch (ev.key) {
|
||||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
case "ArrowDown":
|
||||||
}, [handleKeyDown])
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
setActiveItemIndex(next())
|
||||||
|
setKeyboardActiveIndex(next())
|
||||||
|
break
|
||||||
|
case "ArrowUp":
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
setActiveItemIndex(prev())
|
||||||
|
setKeyboardActiveIndex(prev())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useKeyDown(() => true, handleKeyDown)
|
||||||
|
|
||||||
|
const { setElementRef } = useActiveItemScroll<HTMLAnchorElement>({ activeIndex: keyboardActiveIndex })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
||||||
{!isTablet && <ColumnHeader />}
|
{!isTablet && <ColumnHeader />}
|
||||||
<TopicListItems personalTopics={personalTopics} activeItemIndex={activeItemIndex} />
|
<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}
|
||||||
|
onPointerMove={() => {
|
||||||
|
setKeyboardActiveIndex(null)
|
||||||
|
setActiveItemIndex(index)
|
||||||
|
}}
|
||||||
|
data-keyboard-active={keyboardActiveIndex === index}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Primitive.div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -125,33 +129,3 @@ export const ColumnHeader: React.FC = () => {
|
|||||||
</div>
|
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -12,146 +12,163 @@ import { useAtom } from "jotai"
|
|||||||
import { topicOpenPopoverForIdAtom } from "../list"
|
import { topicOpenPopoverForIdAtom } from "../list"
|
||||||
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
|
||||||
interface TopicItemProps {
|
interface TopicItemProps extends React.HTMLAttributes<HTMLAnchorElement> {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
learningState: LearningStateValue
|
learningState: LearningStateValue
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicItem = React.forwardRef<HTMLDivElement, TopicItemProps>(({ topic, learningState, isActive }, ref) => {
|
export const TopicItem = React.forwardRef<HTMLAnchorElement, TopicItemProps>(
|
||||||
const columnStyles = useColumnStyles()
|
({ topic, learningState, isActive, ...props }, ref) => {
|
||||||
const [openPopoverForId, setOpenPopoverForId] = useAtom(topicOpenPopoverForIdAtom)
|
const columnStyles = useColumnStyles()
|
||||||
const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } })
|
const [openPopoverForId, setOpenPopoverForId] = useAtom(topicOpenPopoverForIdAtom)
|
||||||
|
const router = useRouter()
|
||||||
|
const { me } = useAccount({ root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] } })
|
||||||
|
|
||||||
let p: {
|
let p: {
|
||||||
index: number
|
index: number
|
||||||
topic?: Topic | null
|
topic?: Topic | null
|
||||||
learningState: LearningStateValue
|
learningState: LearningStateValue
|
||||||
} | null = null
|
} | null = null
|
||||||
|
|
||||||
const wantToLearnIndex = me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1
|
const wantToLearnIndex = me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1
|
||||||
if (wantToLearnIndex !== -1) {
|
if (wantToLearnIndex !== -1) {
|
||||||
p = {
|
p = {
|
||||||
index: wantToLearnIndex,
|
index: wantToLearnIndex,
|
||||||
topic: me?.root.topicsWantToLearn[wantToLearnIndex],
|
topic: me?.root.topicsWantToLearn[wantToLearnIndex],
|
||||||
learningState: "wantToLearn"
|
learningState: "wantToLearn"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const learningIndex = me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1
|
|
||||||
if (learningIndex !== -1) {
|
|
||||||
p = {
|
|
||||||
index: learningIndex,
|
|
||||||
topic: me?.root.topicsLearning[learningIndex],
|
|
||||||
learningState: "learning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const learnedIndex = me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1
|
|
||||||
if (learnedIndex !== -1) {
|
|
||||||
p = {
|
|
||||||
index: learnedIndex,
|
|
||||||
topic: me?.root.topicsLearned[learnedIndex],
|
|
||||||
learningState: "learned"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedLearningState = useMemo(() => LEARNING_STATES.find(ls => ls.value === learningState), [learningState])
|
|
||||||
|
|
||||||
const handleLearningStateSelect = useCallback(
|
|
||||||
(value: string) => {
|
|
||||||
const newLearningState = value as LearningStateValue
|
|
||||||
|
|
||||||
const topicLists: Record<LearningStateValue, (ListOfTopics | null) | undefined> = {
|
|
||||||
wantToLearn: me?.root.topicsWantToLearn,
|
|
||||||
learning: me?.root.topicsLearning,
|
|
||||||
learned: me?.root.topicsLearned
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const removeFromList = (state: LearningStateValue, index: number) => {
|
const learningIndex = me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1
|
||||||
topicLists[state]?.splice(index, 1)
|
if (learningIndex !== -1) {
|
||||||
|
p = {
|
||||||
|
index: learningIndex,
|
||||||
|
topic: me?.root.topicsLearning[learningIndex],
|
||||||
|
learningState: "learning"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (p) {
|
const learnedIndex = me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1
|
||||||
if (newLearningState === p.learningState) {
|
if (learnedIndex !== -1) {
|
||||||
removeFromList(p.learningState, p.index)
|
p = {
|
||||||
return
|
index: learnedIndex,
|
||||||
|
topic: me?.root.topicsLearned[learnedIndex],
|
||||||
|
learningState: "learned"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedLearningState = useMemo(() => LEARNING_STATES.find(ls => ls.value === learningState), [learningState])
|
||||||
|
|
||||||
|
const handleLearningStateSelect = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const newLearningState = value as LearningStateValue
|
||||||
|
|
||||||
|
const topicLists: Record<LearningStateValue, (ListOfTopics | null) | undefined> = {
|
||||||
|
wantToLearn: me?.root.topicsWantToLearn,
|
||||||
|
learning: me?.root.topicsLearning,
|
||||||
|
learned: me?.root.topicsLearned
|
||||||
}
|
}
|
||||||
removeFromList(p.learningState, p.index)
|
|
||||||
}
|
|
||||||
|
|
||||||
topicLists[newLearningState]?.push(topic)
|
const removeFromList = (state: LearningStateValue, index: number) => {
|
||||||
|
topicLists[state]?.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
setOpenPopoverForId(null)
|
if (p) {
|
||||||
},
|
if (newLearningState === p.learningState) {
|
||||||
[setOpenPopoverForId, me?.root.topicsWantToLearn, me?.root.topicsLearning, me?.root.topicsLearned, p, topic]
|
removeFromList(p.learningState, p.index)
|
||||||
)
|
return
|
||||||
|
}
|
||||||
|
removeFromList(p.learningState, p.index)
|
||||||
|
}
|
||||||
|
|
||||||
const handlePopoverTriggerClick = (e: React.MouseEvent) => {
|
topicLists[newLearningState]?.push(topic)
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
|
|
||||||
setOpenPopoverForId(openPopoverForId === topic.id ? null : topic.id)
|
setOpenPopoverForId(null)
|
||||||
}
|
},
|
||||||
|
[setOpenPopoverForId, me?.root.topicsWantToLearn, me?.root.topicsLearning, me?.root.topicsLearned, p, topic]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
const handlePopoverTriggerClick = (e: React.MouseEvent) => {
|
||||||
<div
|
e.preventDefault()
|
||||||
ref={ref}
|
e.stopPropagation()
|
||||||
className={cn("relative block", "min-h-12 py-2 max-lg:px-5 sm:px-6", {
|
|
||||||
"bg-muted-foreground/5": isActive,
|
setOpenPopoverForId(openPopoverForId === topic.id ? null : topic.id)
|
||||||
"hover:bg-muted/50": !isActive
|
}
|
||||||
})}
|
|
||||||
role="listitem"
|
const handleKeyDown = React.useCallback(
|
||||||
>
|
(ev: React.KeyboardEvent<HTMLAnchorElement>) => {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
ev.preventDefault()
|
||||||
|
ev.stopPropagation()
|
||||||
|
router.push(`/${topic.name}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[router, topic.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
ref={ref}
|
||||||
href={`/${topic.name}`}
|
href={`/${topic.name}`}
|
||||||
className="flex h-full cursor-default items-center gap-4 outline-none"
|
|
||||||
tabIndex={isActive ? 0 : -1}
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
className={cn(
|
||||||
|
"relative block cursor-default outline-none",
|
||||||
|
"min-h-12 py-2 max-lg:px-4 sm:px-6",
|
||||||
|
"data-[active='true']:bg-[var(--link-background-muted)] data-[keyboard-active='true']:focus-visible:shadow-[var(--link-shadow)_0px_0px_0px_1px_inset]"
|
||||||
|
)}
|
||||||
|
aria-selected={isActive}
|
||||||
|
data-active={isActive}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
{...props}
|
||||||
>
|
>
|
||||||
<Column.Wrapper style={columnStyles.title}>
|
<div className="flex h-full cursor-default items-center gap-4 outline-none" tabIndex={isActive ? 0 : -1}>
|
||||||
<Column.Text className="truncate text-[13px] font-medium">{topic.prettyName}</Column.Text>
|
<Column.Wrapper style={columnStyles.title}>
|
||||||
</Column.Wrapper>
|
<Column.Text className="truncate text-[13px] font-medium">{topic.prettyName}</Column.Text>
|
||||||
|
</Column.Wrapper>
|
||||||
|
|
||||||
<Column.Wrapper style={columnStyles.topic} className="max-sm:justify-end">
|
<Column.Wrapper style={columnStyles.topic} className="max-sm:justify-end">
|
||||||
<Popover
|
<Popover
|
||||||
open={openPopoverForId === topic.id}
|
open={openPopoverForId === topic.id}
|
||||||
onOpenChange={(open: boolean) => setOpenPopoverForId(open ? topic.id : null)}
|
onOpenChange={(open: boolean) => setOpenPopoverForId(open ? topic.id : null)}
|
||||||
>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
role="combobox"
|
|
||||||
variant="secondary"
|
|
||||||
className="size-7 shrink-0 p-0"
|
|
||||||
onClick={handlePopoverTriggerClick}
|
|
||||||
>
|
|
||||||
{selectedLearningState?.icon ? (
|
|
||||||
<LaIcon name={selectedLearningState.icon} className={cn(selectedLearningState.className)} />
|
|
||||||
) : (
|
|
||||||
<LaIcon name="Circle" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-52 rounded-lg p-0"
|
|
||||||
side="bottom"
|
|
||||||
align="end"
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<LearningStateSelectorContent
|
<PopoverTrigger asChild>
|
||||||
showSearch={false}
|
<Button
|
||||||
searchPlaceholder="Search state..."
|
size="sm"
|
||||||
value={learningState}
|
type="button"
|
||||||
onSelect={handleLearningStateSelect}
|
role="combobox"
|
||||||
/>
|
variant="secondary"
|
||||||
</PopoverContent>
|
className="size-7 shrink-0 p-0"
|
||||||
</Popover>
|
onClick={handlePopoverTriggerClick}
|
||||||
</Column.Wrapper>
|
>
|
||||||
|
{selectedLearningState?.icon ? (
|
||||||
|
<LaIcon name={selectedLearningState.icon} className={cn(selectedLearningState.className)} />
|
||||||
|
) : (
|
||||||
|
<LaIcon name="Circle" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-52 rounded-lg p-0"
|
||||||
|
side="bottom"
|
||||||
|
align="end"
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<LearningStateSelectorContent
|
||||||
|
showSearch={false}
|
||||||
|
searchPlaceholder="Search state..."
|
||||||
|
value={learningState}
|
||||||
|
onSelect={handleLearningStateSelect}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</Column.Wrapper>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
)
|
||||||
)
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
TopicItem.displayName = "TopicItem"
|
TopicItem.displayName = "TopicItem"
|
||||||
|
|||||||
Reference in New Issue
Block a user