Merge branch 'main' of github.com:learn-anything/learn-anything.xyz

This commit is contained in:
Aslam H
2024-09-08 13:39:18 +07:00
4 changed files with 149 additions and 150 deletions
@@ -10,18 +10,21 @@ import { useAccount } from "@/lib/providers/jazz-provider"
import { searchSafeRegExp, toTitleCase } from "@/lib/utils" import { searchSafeRegExp, toTitleCase } from "@/lib/utils"
import { GraphNode } from "@/components/routes/public/PublicHomeRoute" import { GraphNode } from "@/components/routes/public/PublicHomeRoute"
import { useCommandActions } from "./hooks/use-command-actions" import { useCommandActions } from "./hooks/use-command-actions"
import { atom, useAtom } from "jotai"
let graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default) let graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default)
const filterItems = (items: CommandItemType[], searchRegex: RegExp) => const filterItems = (items: CommandItemType[], searchRegex: RegExp) =>
items.filter(item => searchRegex.test(item.value)).slice(0, 6) items.filter(item => searchRegex.test(item.value)).slice(0, 6)
export const commandPaletteOpenAtom = atom(false)
export function CommandPalette() { export function CommandPalette() {
const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } }) const { me } = useAccount({ root: { personalLinks: [], personalPages: [] } })
const dialogRef = React.useRef<HTMLDivElement | null>(null) const dialogRef = React.useRef<HTMLDivElement | null>(null)
const [inputValue, setInputValue] = React.useState("") const [inputValue, setInputValue] = React.useState("")
const [activePage, setActivePage] = React.useState("home") const [activePage, setActivePage] = React.useState("home")
const [open, setOpen] = React.useState(false) const [open, setOpen] = useAtom(commandPaletteOpenAtom)
const actions = useCommandActions() const actions = useCommandActions()
const commandGroups = React.useMemo(() => me && createCommandGroups(actions, me), [actions, me]) const commandGroups = React.useMemo(() => me && createCommandGroups(actions, me), [actions, me])
@@ -38,7 +41,7 @@ export function CommandPalette() {
document.addEventListener("keydown", down) document.addEventListener("keydown", down)
return () => document.removeEventListener("keydown", down) return () => document.removeEventListener("keydown", down)
}, []) }, [setOpen])
const bounce = React.useCallback(() => { const bounce = React.useCallback(() => {
if (dialogRef.current) { if (dialogRef.current) {
@@ -177,7 +180,7 @@ export function CommandPalette() {
closeDialog() closeDialog()
} }
}, },
[bounce] [bounce, setOpen]
) )
const filteredCommands = React.useMemo(() => getFilteredCommands(), [getFilteredCommands]) const filteredCommands = React.useMemo(() => getFilteredCommands(), [getFilteredCommands])
+3 -2
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,7 +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 />
<LinkList /> <LinkList key={nuqsEditId} activeItemIndex={activeItemIndex} setActiveItemIndex={setActiveItemIndex} />
<LinkBottomBar /> <LinkBottomBar />
</div> </div>
) )
+66 -55
View File
@@ -1,5 +1,4 @@
"use client" import React, { useCallback, useEffect, useMemo } from "react"
import { import {
DndContext, DndContext,
closestCenter, closestCenter,
@@ -11,6 +10,7 @@ import {
DragStartEvent, DragStartEvent,
UniqueIdentifier UniqueIdentifier
} from "@dnd-kit/core" } from "@dnd-kit/core"
import { Primitive } from "@radix-ui/react-primitive"
import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable" import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy } from "@dnd-kit/sortable"
import { useAccount } from "@/lib/providers/jazz-provider" import { useAccount } from "@/lib/providers/jazz-provider"
import { PersonalLinkLists } from "@/lib/schema/personal-link" import { PersonalLinkLists } from "@/lib/schema/personal-link"
@@ -18,15 +18,20 @@ import { useAtom } from "jotai"
import { linkSortAtom } from "@/store/link" import { linkSortAtom } from "@/store/link"
import { useKey } from "react-use" import { useKey } from "react-use"
import { LinkItem } from "./partials/link-item" import { LinkItem } from "./partials/link-item"
import { useRef, useState, useCallback, useEffect, useMemo } from "react"
import { learningStateAtom } from "./header"
import { useQueryState } from "nuqs" import { useQueryState } from "nuqs"
import { learningStateAtom } from "./header"
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 [editId, setEditId] = useQueryState("editId") const [editId, setEditId] = useQueryState("editId")
const [activeLearningState] = useAtom(learningStateAtom) const [activeLearningState] = useAtom(learningStateAtom)
const [draggingId, setDraggingId] = React.useState<UniqueIdentifier | null>(null)
const { me } = useAccount({ const { me } = useAccount({
root: { personalLinks: [] } root: { personalLinks: [] }
@@ -34,9 +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 [focusedId, setFocusedId] = useState<string | null>(null)
const [draggingId, setDraggingId] = useState<UniqueIdentifier | null>(null)
const linkRefs = useRef<{ [key: string]: HTMLLIElement | null }>({})
const filteredLinks = useMemo( const filteredLinks = useMemo(
() => () =>
@@ -67,16 +69,22 @@ const LinkList: React.FC<LinkListProps> = () => {
}) })
) )
const registerRef = useCallback((id: string, ref: HTMLLIElement | null) => {
linkRefs.current[id] = ref
}, [])
useKey("Escape", () => { useKey("Escape", () => {
if (editId) { if (editId) {
setEditId(null) setEditId(null)
} }
}) })
// 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) {
@@ -87,57 +95,56 @@ const LinkList: React.FC<LinkListProps> = () => {
useEffect(() => { useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (!me?.root?.personalLinks || sortedLinks.length === 0 || editId !== null) return if (isCommandPalettePpen || !me?.root?.personalLinks || sortedLinks.length === 0 || editId !== null) return
const currentIndex = sortedLinks.findIndex(link => link?.id === focusedId)
if (e.key === "ArrowUp" || e.key === "ArrowDown") { if (e.key === "ArrowUp" || e.key === "ArrowDown") {
e.preventDefault() e.preventDefault()
const newIndex = setActiveItemIndex(prevIndex => {
e.key === "ArrowUp" ? Math.max(0, currentIndex - 1) : Math.min(sortedLinks.length - 1, currentIndex + 1) if (prevIndex === null) return 0
const newIndex =
e.key === "ArrowUp" ? Math.max(0, prevIndex - 1) : Math.min(sortedLinks.length - 1, prevIndex + 1)
if (e.metaKey && sort === "manual") { if (e.metaKey && sort === "manual") {
const currentLink = me.root.personalLinks[currentIndex] const linksArray = [...me.root.personalLinks]
if (!currentLink) return const newLinks = arrayMove(linksArray, prevIndex, newIndex)
const linksArray = [...me.root.personalLinks] while (me.root.personalLinks.length > 0) {
const newLinks = arrayMove(linksArray, currentIndex, newIndex) me.root.personalLinks.pop()
while (me.root.personalLinks.length > 0) {
me.root.personalLinks.pop()
}
newLinks.forEach(link => {
if (link) {
me.root.personalLinks.push(link)
} }
})
updateSequences(me.root.personalLinks) newLinks.forEach(link => {
if (link) {
const newFocusedLink = me.root.personalLinks[newIndex] me.root.personalLinks.push(link)
if (newFocusedLink) { }
setFocusedId(newFocusedLink.id)
requestAnimationFrame(() => {
linkRefs.current[newFocusedLink.id]?.focus()
})
}
} else {
const newFocusedLink = sortedLinks[newIndex]
if (newFocusedLink) {
setFocusedId(newFocusedLink.id)
requestAnimationFrame(() => {
linkRefs.current[newFocusedLink.id]?.focus()
}) })
updateSequences(me.root.personalLinks)
} }
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, focusedId, editId, sort, updateSequences]) }, [
me?.root?.personalLinks,
sortedLinks,
editId,
sort,
updateSequences,
isCommandPalettePpen,
activeItemIndex,
setEditId,
setActiveItemIndex
])
const handleDragStart = useCallback( const handleDragStart = useCallback(
(event: DragStartEvent) => { (event: DragStartEvent) => {
@@ -185,6 +192,7 @@ const LinkList: React.FC<LinkListProps> = () => {
}) })
updateSequences(me.root.personalLinks) updateSequences(me.root.personalLinks)
setActiveItemIndex(newIndex)
} catch (error) { } catch (error) {
console.error("Error during link reordering:", error) console.error("Error during link reordering:", error)
} }
@@ -194,7 +202,10 @@ const LinkList: React.FC<LinkListProps> = () => {
} }
return ( return (
<div className="mb-14 flex w-full flex-1 flex-col overflow-y-auto [scrollbar-gutter:stable]"> <Primitive.div
className="mb-14 flex w-full flex-1 flex-col overflow-y-auto outline-none [scrollbar-gutter:stable]"
tabIndex={0}
>
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}
@@ -204,7 +215,7 @@ const LinkList: React.FC<LinkListProps> = () => {
<SortableContext items={sortedLinks.map(item => item?.id || "") || []} strategy={verticalListSortingStrategy}> <SortableContext items={sortedLinks.map(item => item?.id || "") || []} strategy={verticalListSortingStrategy}>
<ul role="list" className="divide-primary/5 divide-y"> <ul role="list" className="divide-primary/5 divide-y">
{sortedLinks.map( {sortedLinks.map(
linkItem => (linkItem, index) =>
linkItem && ( linkItem && (
<LinkItem <LinkItem
key={linkItem.id} key={linkItem.id}
@@ -212,17 +223,17 @@ const LinkList: React.FC<LinkListProps> = () => {
setEditId={setEditId} setEditId={setEditId}
personalLink={linkItem} personalLink={linkItem}
disabled={sort !== "manual" || editId !== null} disabled={sort !== "manual" || editId !== null}
registerRef={registerRef}
isDragging={draggingId === linkItem.id} isDragging={draggingId === linkItem.id}
isFocused={focusedId === linkItem.id} isActive={activeItemIndex === index}
setFocusedId={setFocusedId} setActiveItemIndex={setActiveItemIndex}
index={index}
/> />
) )
)} )}
</ul> </ul>
</SortableContext> </SortableContext>
</DndContext> </DndContext>
</div> </Primitive.div>
) )
} }
@@ -1,5 +1,3 @@
"use client"
import React, { useCallback, useMemo } from "react" import React, { useCallback, useMemo } from "react"
import Image from "next/image" import Image from "next/image"
import Link from "next/link" import Link from "next/link"
@@ -15,7 +13,7 @@ import { PersonalLink } from "@/lib/schema/personal-link"
import { LinkForm } from "./form/link-form" import { LinkForm } from "./form/link-form"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants" import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
import { linkOpenPopoverForIdAtom, linkShowCreateAtom } from "@/store/link" import { linkOpenPopoverForIdAtom } from "@/store/link"
interface LinkItemProps { interface LinkItemProps {
personalLink: PersonalLink personalLink: PersonalLink
@@ -23,9 +21,9 @@ interface LinkItemProps {
isEditing: boolean isEditing: boolean
setEditId: (id: string | null) => void setEditId: (id: string | null) => void
isDragging: boolean isDragging: boolean
isFocused: boolean isActive: boolean
setFocusedId: (id: string | null) => void setActiveItemIndex: (index: number | null) => void
registerRef: (id: string, ref: HTMLLIElement | null) => void index: number
} }
export const LinkItem: React.FC<LinkItemProps> = ({ export const LinkItem: React.FC<LinkItemProps> = ({
@@ -34,9 +32,9 @@ export const LinkItem: React.FC<LinkItemProps> = ({
personalLink, personalLink,
disabled = false, disabled = false,
isDragging, isDragging,
isFocused, isActive,
setFocusedId, setActiveItemIndex,
registerRef index
}) => { }) => {
const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom) const [openPopoverForId, setOpenPopoverForId] = useAtom(linkOpenPopoverForIdAtom)
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled }) const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: personalLink.id, disabled })
@@ -50,14 +48,6 @@ export const LinkItem: React.FC<LinkItemProps> = ({
[transform, transition, isDragging] [transform, transition, isDragging]
) )
const refCallback = useCallback(
(node: HTMLLIElement | null) => {
setNodeRef(node)
registerRef(personalLink.id, node)
},
[setNodeRef, registerRef, personalLink.id]
)
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => { (e: React.KeyboardEvent) => {
if (e.key === "Enter") { if (e.key === "Enter") {
@@ -92,90 +82,84 @@ export const LinkItem: React.FC<LinkItemProps> = ({
return ( return (
<li <li
ref={refCallback} ref={setNodeRef}
style={style as React.CSSProperties} style={style as React.CSSProperties}
{...attributes} {...attributes}
{...listeners} {...listeners}
tabIndex={0} tabIndex={0}
onFocus={() => setFocusedId(personalLink.id)} onFocus={() => setActiveItemIndex(index)}
onBlur={() => setFocusedId(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(
"bg-muted-foreground/10": isFocused, "relative cursor-default outline-none",
"hover:bg-muted/50": !isFocused "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,
"hover:bg-muted/50": !isActive
}
)}
onDoubleClick={handleRowDoubleClick} onDoubleClick={handleRowDoubleClick}
> >
<div className="flex grow justify-between gap-x-6 px-6 max-lg:px-4"> <Popover
<div className="flex min-w-0 items-center gap-x-4"> open={openPopoverForId === personalLink.id}
<Popover onOpenChange={(open: boolean) => setOpenPopoverForId(open ? personalLink.id : null)}
open={openPopoverForId === personalLink.id} >
onOpenChange={(open: boolean) => setOpenPopoverForId(open ? personalLink.id : null)} <PopoverTrigger asChild>
> <Button size="sm" type="button" role="combobox" variant="secondary" className="size-7 shrink-0 p-0">
<PopoverTrigger asChild> {selectedLearningState?.icon ? (
<Button size="sm" type="button" role="combobox" variant="secondary" className="size-7 shrink-0 p-0"> <LaIcon name={selectedLearningState.icon} className={cn(selectedLearningState.className)} />
{selectedLearningState?.icon ? ( ) : (
<LaIcon name={selectedLearningState.icon} className={cn(selectedLearningState.className)} /> <LaIcon name="Circle" />
) : ( )}
<LaIcon name="Circle" /> </Button>
)} </PopoverTrigger>
</Button> <PopoverContent
</PopoverTrigger> className="w-52 rounded-lg p-0"
<PopoverContent side="bottom"
className="w-52 rounded-lg p-0" align="start"
side="bottom" onCloseAutoFocus={e => e.preventDefault()}
align="start" >
onCloseAutoFocus={e => e.preventDefault()} <LearningStateSelectorContent
> showSearch={false}
<LearningStateSelectorContent searchPlaceholder="Search state..."
showSearch={false} value={personalLink.learningState}
searchPlaceholder="Search state..." onSelect={handleLearningStateSelect}
value={personalLink.learningState} />
onSelect={handleLearningStateSelect} </PopoverContent>
/> </Popover>
</PopoverContent>
</Popover>
{personalLink.icon && ( <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">
<Image {personalLink.icon && (
src={personalLink.icon} <Image
alt={personalLink.title} src={personalLink.icon}
className="size-5 rounded-full" alt={personalLink.title}
width={16} className="size-5 shrink-0 rounded-full"
height={16} width={16}
/> height={16}
)} />
<div className="w-full min-w-0 flex-auto"> )}
<div className="gap-x-2 space-y-0.5 xl:flex xl:flex-row"> <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">
<p className="text-primary hover:text-primary line-clamp-1 text-sm font-medium xl:truncate"> <p className="text-primary hover:text-primary truncate text-sm font-medium">{personalLink.title}</p>
{personalLink.title} {personalLink.url && (
</p> <div className="text-muted-foreground flex min-w-0 shrink items-center gap-x-1">
{personalLink.url && ( <LaIcon name="Link" aria-hidden="true" className="size-3 flex-none" />
<div className="group flex items-center gap-x-1"> <Link
<LaIcon href={personalLink.url}
name="Link" passHref
aria-hidden="true" prefetch={false}
className="text-muted-foreground group-hover:text-primary flex-none" target="_blank"
/> onClick={e => e.stopPropagation()}
<Link className="hover:text-primary truncate text-xs"
href={personalLink.url} >
passHref {personalLink.url}
prefetch={false} </Link>
target="_blank"
onClick={e => e.stopPropagation()}
className="text-muted-foreground hover:text-primary text-xs"
>
<span className="xl:truncate">{personalLink.url}</span>
</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>
) )