fix: Link, Pages, Topic, Hook and Others (#178)

* chore: remove useKeyDownListener

* chore: remove react-use, update jazz version and add query string

* chore: update jazz version

* chore: use simple mac or win utils code

* feat(util): add isTextInput

* feat(hooks): all needed hooks

* fix: link bunch stuff

* fix: page bunch stuff

* chore: bunch update for custom component

* chore: use throttle from internal hook

* chore: topic bunch stuff

* chore: update layout

* fix: truncate content header of topic detail
This commit is contained in:
Aslam
2024-09-23 23:16:02 +07:00
committed by GitHub
parent 21084cd3f3
commit 867478d55c
43 changed files with 616 additions and 466 deletions

View File

@@ -38,6 +38,14 @@ const SHORTCUTS: ShortcutSection[] = [
{ label: "Go to page", keys: ["G"], then: ["P"] },
{ label: "Go to topic", keys: ["G"], then: ["T"] }
]
},
{
title: "Links",
shortcuts: [{ label: "Create new link", keys: ["c"] }]
},
{
title: "Pages",
shortcuts: [{ label: "Create new page", keys: ["p"] }]
}
]

View File

@@ -11,7 +11,6 @@ import { searchSafeRegExp } from "@/lib/utils"
import { GraphNode } from "@/components/routes/public/PublicHomeRoute"
import { useCommandActions } from "./hooks/use-command-actions"
import { atom, useAtom } from "jotai"
import { useKeydownListener } from "@/hooks/use-keydown-listener"
const graph_data_promise = import("@/components/routes/public/graph-data.json").then(a => a.default)
@@ -40,18 +39,6 @@ export function RealCommandPalette() {
const raw_graph_data = React.use(graph_data_promise) as GraphNode[]
const handleKeydown = React.useCallback(
(e: KeyboardEvent) => {
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen(prev => !prev)
}
},
[setOpen]
)
useKeydownListener(handleKeydown)
const bounce = React.useCallback(() => {
if (dialogRef.current) {
dialogRef.current.style.transform = "scale(0.99) translateX(-50%)"

View File

@@ -1,12 +1,12 @@
"use client"
import React from "react"
import * as React from "react"
import { Button } from "../ui/button"
import { PanelLeftIcon } from "lucide-react"
import { useAtom } from "jotai"
import { isCollapseAtom, toggleCollapseAtom } from "@/store/sidebar"
import { useMedia } from "react-use"
import { useMedia } from "@/hooks/use-media"
import { cn } from "@/lib/utils"
import { LaIcon } from "./la-icon"
type ContentHeaderProps = Omit<React.HTMLAttributes<HTMLDivElement>, "title">
@@ -15,7 +15,7 @@ export const ContentHeader = React.forwardRef<HTMLDivElement, ContentHeaderProps
return (
<header
className={cn(
"flex min-h-10 min-w-0 max-w-[100vw] shrink-0 items-center gap-3 pl-8 pr-6 transition-opacity max-lg:px-4",
"flex min-h-10 min-w-0 shrink-0 items-center gap-3 pl-8 pr-6 transition-opacity max-lg:px-4",
className
)}
ref={ref}
@@ -52,7 +52,7 @@ export const SidebarToggleButton: React.FC = () => {
className="text-primary/60"
onClick={handleClick}
>
<PanelLeftIcon size={16} />
<LaIcon name="PanelLeft" />
</Button>
</div>
)

View File

@@ -0,0 +1,130 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { useKeyDown, KeyFilter, Options } from "@/hooks/use-key-down"
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { useRouter } from "next/navigation"
import queryString from "query-string"
import { usePageActions } from "../routes/page/hooks/use-page-actions"
import { useAuth } from "@clerk/nextjs"
import { isModKey } from "@/lib/utils"
import { useAtom } from "jotai"
import { commandPaletteOpenAtom } from "./command-palette/command-palette"
type RegisterKeyDownProps = {
trigger: KeyFilter
handler: (event: KeyboardEvent) => void
options?: Options
}
function RegisterKeyDown({ trigger, handler, options }: RegisterKeyDownProps) {
useKeyDown(trigger, handler, options)
return null
}
type Sequence = {
[key: string]: string
}
const SEQUENCES: Sequence = {
GL: "/links",
GP: "/pages",
GT: "/topics"
}
const MAX_SEQUENCE_TIME = 1000
export function GlobalKeyboardHandler() {
const [openCommandPalette, setOpenCommandPalette] = useAtom(commandPaletteOpenAtom)
const [sequence, setSequence] = useState<string[]>([])
const { signOut } = useAuth()
const router = useRouter()
const { me } = useAccountOrGuest()
const { newPage } = usePageActions()
const resetSequence = useCallback(() => {
setSequence([])
}, [])
const checkSequence = useCallback(() => {
const sequenceStr = sequence.join("")
const route = SEQUENCES[sequenceStr]
if (route) {
console.log(`Navigating to ${route}...`)
router.push(route)
resetSequence()
}
}, [sequence, router, resetSequence])
const goToNewLink = useCallback(
(event: KeyboardEvent) => {
if (event.metaKey || event.altKey) {
return
}
router.push(`/links?${queryString.stringify({ create: true })}`)
},
[router]
)
const goToNewPage = useCallback(
(event: KeyboardEvent) => {
if (event.metaKey || event.altKey) {
return
}
if (!me || me._type === "Anonymous") {
return
}
const page = newPage(me)
router.push(`/pages/${page.id}`)
},
[me, newPage, router]
)
useKeyDown(
e => e.altKey && e.shiftKey && e.code === "KeyQ",
() => {
signOut()
}
)
useKeyDown(
() => true,
e => {
const key = e.key.toUpperCase()
setSequence(prev => [...prev, key])
}
)
useKeyDown(
e => isModKey(e) && e.code === "KeyK",
e => {
e.preventDefault()
setOpenCommandPalette(prev => !prev)
}
)
useEffect(() => {
checkSequence()
const timeoutId = setTimeout(() => {
resetSequence()
}, MAX_SEQUENCE_TIME)
return () => clearTimeout(timeoutId)
}, [sequence, checkSequence, resetSequence])
return (
me &&
me._type !== "Anonymous" && (
<>
<RegisterKeyDown trigger="c" handler={goToNewLink} />
<RegisterKeyDown trigger="p" handler={goToNewPage} />
</>
)
)
}

View File

@@ -1,63 +0,0 @@
"use client"
import { useState, useEffect, useCallback } from "react"
import { useKeydownListener } from "@/hooks/use-keydown-listener"
import { useAuth } from "@clerk/nextjs"
import { useRouter } from "next/navigation"
type Sequence = {
[key: string]: string
}
const SEQUENCES: Sequence = {
GL: "/links",
GP: "/pages",
GT: "/topics"
}
const MAX_SEQUENCE_TIME = 1000
export function GlobalKeydownHandler() {
const [sequence, setSequence] = useState<string[]>([])
const { signOut } = useAuth()
const router = useRouter()
const resetSequence = useCallback(() => {
setSequence([])
}, [])
const checkSequence = useCallback(() => {
const sequenceStr = sequence.join("")
const route = SEQUENCES[sequenceStr]
if (route) {
console.log(`Navigating to ${route}...`)
router.push(route)
resetSequence()
}
}, [sequence, router, resetSequence])
useKeydownListener((e: KeyboardEvent) => {
// Check for logout shortcut
if (e.altKey && e.shiftKey && e.code === "KeyQ") {
signOut()
return
}
// Key sequence handling
const key = e.key.toUpperCase()
setSequence(prev => [...prev, key])
})
useEffect(() => {
checkSequence()
const timeoutId = setTimeout(() => {
resetSequence()
}, MAX_SEQUENCE_TIME)
return () => clearTimeout(timeoutId)
}, [sequence, checkSequence, resetSequence])
return null
}

View File

@@ -8,6 +8,7 @@ import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
import { linkLearningStateSelectorAtom } from "@/store/link"
import { Command, CommandInput, CommandList, CommandItem, CommandGroup } from "@/components/ui/command"
import { ScrollArea } from "@/components/ui/scroll-area"
import type { icons } from "lucide-react"
interface LearningStateSelectorProps {
showSearch?: boolean
@@ -16,6 +17,7 @@ interface LearningStateSelectorProps {
value?: string
onChange: (value: LearningStateValue) => void
className?: string
defaultIcon?: keyof typeof icons
}
export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
@@ -24,7 +26,8 @@ export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
searchPlaceholder = "Search state...",
value,
onChange,
className
className,
defaultIcon
}) => {
const [isLearningStateSelectorOpen, setIsLearningStateSelectorOpen] = useAtom(linkLearningStateSelectorAtom)
const selectedLearningState = useMemo(() => LEARNING_STATES.find(ls => ls.value === value), [value])
@@ -44,21 +47,24 @@ export const LearningStateSelector: React.FC<LearningStateSelectorProps> = ({
variant="secondary"
className={cn("gap-x-2 text-sm", className)}
>
{selectedLearningState?.icon && (
<LaIcon name={selectedLearningState.icon} className={cn(selectedLearningState.className)} />
)}
<span className={cn("truncate", selectedLearningState?.className || "")}>
{selectedLearningState?.label || defaultLabel}
</span>
{selectedLearningState?.icon ||
(defaultIcon && (
<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" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-52 rounded-lg p-0"
side="bottom"
align="end"
onCloseAutoFocus={e => e.preventDefault()}
>
<PopoverContent className="w-52 rounded-lg p-0" side="bottom" align="end">
<LearningStateSelectorContent
showSearch={showSearch}
searchPlaceholder={searchPlaceholder}

View File

@@ -81,10 +81,7 @@ const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({ pageCount, isActi
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 focus-visible:outline-none focus-visible:ring-0"
>
<Link href="/pages" className="flex flex-1 items-center justify-start rounded-md px-2 py-1">
<p className="text-xs">
Pages
{pageCount > 0 && <span className="text-muted-foreground ml-1">{pageCount}</span>}

View File

@@ -1,6 +1,6 @@
"use client"
import { useEffect, useState } from "react"
import * as React from "react"
import { SignInButton, useAuth, useUser } from "@clerk/nextjs"
import { useAtom } from "jotai"
import Link from "next/link"
@@ -27,13 +27,13 @@ import { useKeyboardManager } from "@/hooks/use-keyboard-manager"
export const ProfileSection: React.FC = () => {
const { user, isSignedIn } = useUser()
const { signOut } = useAuth()
const [menuOpen, setMenuOpen] = useState(false)
const [menuOpen, setMenuOpen] = React.useState(false)
const pathname = usePathname()
const [, setShowShortcut] = useAtom(showShortcutAtom)
const { disableKeydown } = useKeyboardManager("profileSection")
useEffect(() => {
React.useEffect(() => {
disableKeydown(menuOpen)
}, [menuOpen, disableKeydown])

View File

@@ -3,9 +3,8 @@
import * as React from "react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useMedia } from "react-use"
import { useMedia } from "@/hooks/use-media"
import { useAtom } from "jotai"
import { SearchIcon } from "lucide-react"
import { Logo } from "@/components/custom/logo"
import { Button } from "@/components/ui/button"
import { cn } from "@/lib/utils"
@@ -15,6 +14,7 @@ import { PageSection } from "./partial/page-section"
import { TopicSection } from "./partial/topic-section"
import { ProfileSection } from "./partial/profile-section"
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
import { LaIcon } from "../la-icon"
interface SidebarContextType {
isCollapsed: boolean
@@ -98,7 +98,7 @@ const LogoAndSearch: React.FC = React.memo(() => {
type="button"
className="text-primary/60 flex w-20 items-center justify-start py-4 pl-2"
>
<SearchIcon size={16} className="mr-2" />
<LaIcon name="Search" className="mr-2" />
</Button>
</Link>
)}
@@ -119,11 +119,11 @@ const SidebarContent: React.FC = React.memo(() => {
<div>
<LogoAndSearch />
</div>
<div tabIndex={-1} className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3">
<div className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3 outline-none">
<div className="h-2 shrink-0" />
{me._type === "Account" && <LinkSection pathname={pathname} />}
{me._type === "Account" && <PageSection pathname={pathname} />}
{me._type === "Account" && <TopicSection pathname={pathname} />}
{me._type === "Account" && <PageSection pathname={pathname} />}
</div>
<ProfileSection />

View File

@@ -79,12 +79,7 @@ export const TopicSelector = forwardRef<HTMLButtonElement, TopicSelectorProps>(
<LaIcon name="ChevronDown" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-52 rounded-lg p-0"
side={side}
align={align}
onCloseAutoFocus={e => e.preventDefault()}
>
<PopoverContent className="w-52 rounded-lg p-0" side={side} align={align}>
{group?.root.topics && (
<TopicSelectorContent
showSearch={showSearch}