mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
fix(page): Add item scroll, fix display issues, refactor nav, and improve perf (#166)
* feat: add item scroll to active * fix: reset enterkey and scroll to view * fix: link item displayName * refactor: remove keyboard page nav * chore: fix scrolling, perf, keys, highlight active item etc * chore: use new hook for create a page * chore: disabled auto delete page
This commit is contained in:
@@ -3,11 +3,13 @@ import { ensureUrlProtocol } from "@/lib/utils"
|
||||
import { useTheme } from "next-themes"
|
||||
import { toast } from "sonner"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { LaAccount, PersonalPage } from "@/lib/schema"
|
||||
import { LaAccount } from "@/lib/schema"
|
||||
import { usePageActions } from "@/components/routes/page/hooks/use-page-actions"
|
||||
|
||||
export const useCommandActions = () => {
|
||||
const { setTheme } = useTheme()
|
||||
const router = useRouter()
|
||||
const { newPage } = usePageActions()
|
||||
|
||||
const changeTheme = React.useCallback(
|
||||
(theme: string) => {
|
||||
@@ -35,19 +37,10 @@ export const useCommandActions = () => {
|
||||
|
||||
const createNewPage = React.useCallback(
|
||||
(me: LaAccount) => {
|
||||
try {
|
||||
const newPersonalPage = PersonalPage.create(
|
||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ owner: me._owner }
|
||||
)
|
||||
|
||||
me.root?.personalPages?.push(newPersonalPage)
|
||||
router.push(`/pages/${newPersonalPage.id}`)
|
||||
} catch (error) {
|
||||
toast.error("Failed to create page")
|
||||
}
|
||||
const page = newPage(me)
|
||||
router.push(`/pages/${page.id}`)
|
||||
},
|
||||
[router]
|
||||
[router, newPage]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { atomWithStorage } from "jotai/utils"
|
||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema/personal-page"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { toast } from "sonner"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
DropdownMenuTrigger
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { icons } from "lucide-react"
|
||||
import { usePageActions } from "@/components/routes/page/hooks/use-page-actions"
|
||||
|
||||
type SortOption = "title" | "recent"
|
||||
type ShowOption = 5 | 10 | 15 | 20 | 0
|
||||
@@ -101,20 +101,13 @@ const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({ pageCount, isActi
|
||||
const NewPageButton: React.FC = () => {
|
||||
const { me } = useAccount()
|
||||
const router = useRouter()
|
||||
const { newPage } = usePageActions()
|
||||
|
||||
if (!me) return null
|
||||
|
||||
const handleClick = () => {
|
||||
try {
|
||||
const newPersonalPage = PersonalPage.create(
|
||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ owner: me._owner }
|
||||
)
|
||||
me.root?.personalPages?.push(newPersonalPage)
|
||||
router.push(`/pages/${newPersonalPage.id}`)
|
||||
} catch (error) {
|
||||
toast.error("Failed to create page")
|
||||
}
|
||||
const page = newPage(me)
|
||||
router.push(`/pages/${page.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -23,11 +23,11 @@ import { usePageActions } from "../hooks/use-page-actions"
|
||||
|
||||
const TITLE_PLACEHOLDER = "Untitled"
|
||||
|
||||
const emptyPage = (page: PersonalPage): boolean => {
|
||||
const isPageEmpty = (page: PersonalPage): boolean => {
|
||||
return (!page.title || page.title.trim() === "") && (!page.content || Object.keys(page.content).length === 0)
|
||||
}
|
||||
|
||||
export const DeleteEmptyPage = (currentPageId: string | null) => {
|
||||
const useDeleteEmptyPage = (currentPageId: string | null) => {
|
||||
const router = useRouter()
|
||||
const { me } = useAccount({
|
||||
root: {
|
||||
@@ -36,21 +36,17 @@ export const DeleteEmptyPage = (currentPageId: string | null) => {
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const handleRouteChange = () => {
|
||||
return () => {
|
||||
if (!currentPageId || !me?.root?.personalPages) return
|
||||
|
||||
const currentPage = me.root.personalPages.find(page => page?.id === currentPageId)
|
||||
if (currentPage && emptyPage(currentPage)) {
|
||||
if (currentPage && isPageEmpty(currentPage)) {
|
||||
const index = me.root.personalPages.findIndex(page => page?.id === currentPageId)
|
||||
if (index !== -1) {
|
||||
me.root.personalPages.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
handleRouteChange()
|
||||
}
|
||||
}, [currentPageId, me, router])
|
||||
}
|
||||
|
||||
@@ -62,9 +58,9 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
||||
const { deletePage } = usePageActions()
|
||||
const confirm = useConfirm()
|
||||
|
||||
DeleteEmptyPage(pageId)
|
||||
// useDeleteEmptyPage(pageId)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const handleDelete = useCallback(async () => {
|
||||
const result = await confirm({
|
||||
title: "Delete page",
|
||||
description: "Are you sure you want to delete this page?",
|
||||
@@ -78,7 +74,7 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
|
||||
deletePage(me, pageId as ID<PersonalPage>)
|
||||
router.push("/pages")
|
||||
}
|
||||
}
|
||||
}, [confirm, deletePage, me, pageId, router])
|
||||
|
||||
if (!page) return null
|
||||
|
||||
@@ -132,29 +128,32 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
const isContentInitialMount = useRef(true)
|
||||
const isInitialFocusApplied = useRef(false)
|
||||
|
||||
const updatePageContent = (content: Content, model: PersonalPage) => {
|
||||
const updatePageContent = useCallback((content: Content, model: PersonalPage) => {
|
||||
if (isContentInitialMount.current) {
|
||||
isContentInitialMount.current = false
|
||||
return
|
||||
}
|
||||
model.content = content
|
||||
model.updatedAt = new Date()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleUpdateTitle = (editor: Editor) => {
|
||||
if (isTitleInitialMount.current) {
|
||||
isTitleInitialMount.current = false
|
||||
return
|
||||
}
|
||||
const handleUpdateTitle = useCallback(
|
||||
(editor: Editor) => {
|
||||
if (isTitleInitialMount.current) {
|
||||
isTitleInitialMount.current = false
|
||||
return
|
||||
}
|
||||
|
||||
const newTitle = editor.getText()
|
||||
if (newTitle !== page.title) {
|
||||
const slug = generateUniqueSlug(page.title?.toString() || "")
|
||||
page.title = newTitle
|
||||
page.slug = slug
|
||||
page.updatedAt = new Date()
|
||||
}
|
||||
}
|
||||
const newTitle = editor.getText()
|
||||
if (newTitle !== page.title) {
|
||||
const slug = generateUniqueSlug(page.title?.toString() || "")
|
||||
page.title = newTitle
|
||||
page.slug = slug
|
||||
page.updatedAt = new Date()
|
||||
}
|
||||
},
|
||||
[page]
|
||||
)
|
||||
|
||||
const handleTitleKeyDown = useCallback((view: EditorView, event: KeyboardEvent) => {
|
||||
const editor = titleEditorRef.current
|
||||
@@ -254,7 +253,7 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||
contentEditorRef.current.editor.commands.focus()
|
||||
}
|
||||
}
|
||||
}, [page.title, titleEditor, contentEditorRef])
|
||||
}, [page.title, titleEditor])
|
||||
|
||||
return (
|
||||
<div className="relative flex grow flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
||||
|
||||
@@ -1,54 +1,58 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { PersonalPage } from "@/lib/schema"
|
||||
import { toast } from "sonner"
|
||||
import { usePageActions } from "./hooks/use-page-actions"
|
||||
|
||||
export const PageHeader = React.memo(() => {
|
||||
interface PageHeaderProps {}
|
||||
|
||||
export const PageHeader: React.FC<PageHeaderProps> = React.memo(() => {
|
||||
const { me } = useAccount()
|
||||
const router = useRouter()
|
||||
const { newPage } = usePageActions()
|
||||
|
||||
if (!me) return null
|
||||
|
||||
const handleClick = () => {
|
||||
try {
|
||||
const newPersonalPage = PersonalPage.create(
|
||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ owner: me._owner }
|
||||
)
|
||||
me.root?.personalPages?.push(newPersonalPage)
|
||||
router.push(`/pages/${newPersonalPage.id}`)
|
||||
} catch (error) {
|
||||
toast.error("Failed to create page")
|
||||
}
|
||||
const handleNewPageClick = () => {
|
||||
const page = newPage(me)
|
||||
router.push(`/pages/${page.id}`)
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentHeader className="px-6 py-5 max-lg:px-4">
|
||||
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
|
||||
<SidebarToggleButton />
|
||||
<div className="flex min-h-0 items-center">
|
||||
<span className="truncate text-left font-bold lg:text-xl">Pages</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-auto"></div>
|
||||
|
||||
<div className="flex w-auto items-center justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" type="button" variant="secondary" className="gap-x-2" onClick={handleClick}>
|
||||
<LaIcon name="Plus" />
|
||||
<span className="hidden md:block">New page</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ContentHeader className="px-6 py-4 max-lg:px-4">
|
||||
<HeaderTitle />
|
||||
<div className="flex flex-auto" />
|
||||
<NewPageButton onClick={handleNewPageClick} />
|
||||
</ContentHeader>
|
||||
)
|
||||
})
|
||||
|
||||
PageHeader.displayName = "PageHeader"
|
||||
|
||||
const HeaderTitle: React.FC = () => (
|
||||
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
|
||||
<SidebarToggleButton />
|
||||
<div className="flex min-h-0 items-center">
|
||||
<span className="truncate text-left font-bold lg:text-xl">Pages</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
interface NewPageButtonProps {
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
const NewPageButton: React.FC<NewPageButtonProps> = ({ onClick }) => (
|
||||
<div className="flex w-auto items-center justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" type="button" variant="secondary" className="gap-x-2" onClick={onClick}>
|
||||
<LaIcon name="Plus" />
|
||||
<span className="hidden md:block">New page</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import React, { useEffect, useRef, useCallback } from "react"
|
||||
import { PersonalPage, PersonalPageLists } from "@/lib/schema"
|
||||
|
||||
interface UseKeyboardNavigationProps {
|
||||
personalPages?: PersonalPageLists | null
|
||||
activeItemIndex: number | null
|
||||
setActiveItemIndex: React.Dispatch<React.SetStateAction<number | null>>
|
||||
isCommandPaletteOpen: boolean
|
||||
disableEnterKey: boolean
|
||||
onEnter?: (selectedPage: PersonalPage) => void
|
||||
}
|
||||
|
||||
export const useKeyboardNavigation = ({
|
||||
personalPages,
|
||||
activeItemIndex,
|
||||
setActiveItemIndex,
|
||||
isCommandPaletteOpen,
|
||||
disableEnterKey,
|
||||
onEnter
|
||||
}: UseKeyboardNavigationProps) => {
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
const itemRefs = useRef<(HTMLAnchorElement | null)[]>([])
|
||||
const itemCount = personalPages?.length || 0
|
||||
|
||||
const scrollIntoView = useCallback((index: number) => {
|
||||
if (itemRefs.current[index]) {
|
||||
itemRefs.current[index]?.scrollIntoView({
|
||||
block: "nearest"
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeItemIndex !== null) {
|
||||
scrollIntoView(activeItemIndex)
|
||||
}
|
||||
}, [activeItemIndex, scrollIntoView])
|
||||
|
||||
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 && personalPages) {
|
||||
e.preventDefault()
|
||||
const selectedPage = personalPages[activeItemIndex]
|
||||
if (selectedPage) onEnter?.(selectedPage)
|
||||
}
|
||||
},
|
||||
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, onEnter]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [handleKeyDown])
|
||||
|
||||
const setItemRef = useCallback((el: HTMLAnchorElement | null, index: number) => {
|
||||
itemRefs.current[index] = el
|
||||
}, [])
|
||||
|
||||
return { listRef, setItemRef }
|
||||
}
|
||||
@@ -4,6 +4,15 @@ import { LaAccount, PersonalPage } from "@/lib/schema"
|
||||
import { ID } from "jazz-tools"
|
||||
|
||||
export const usePageActions = () => {
|
||||
const newPage = useCallback((me: LaAccount): PersonalPage => {
|
||||
const newPersonalPage = PersonalPage.create(
|
||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
||||
{ owner: me._owner }
|
||||
)
|
||||
me.root?.personalPages?.push(newPersonalPage)
|
||||
return newPersonalPage
|
||||
}, [])
|
||||
|
||||
const deletePage = useCallback((me: LaAccount, pageId: ID<PersonalPage>): void => {
|
||||
if (!me.root?.personalPages) return
|
||||
|
||||
@@ -32,5 +41,5 @@ export const usePageActions = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { deletePage }
|
||||
return { newPage, deletePage }
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import React, { useMemo, useCallback } from "react"
|
||||
import React, { useMemo, useCallback, useEffect } from "react"
|
||||
import { Primitive } from "@radix-ui/react-primitive"
|
||||
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 { useKeyboardNavigation } from "./hooks/use-keyboard-navigation"
|
||||
import { useMedia } from "react-use"
|
||||
import { Column } from "./partials/column"
|
||||
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"
|
||||
|
||||
interface PageListProps {
|
||||
activeItemIndex: number | null
|
||||
@@ -23,6 +23,7 @@ export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveIt
|
||||
const { me } = useAccount({ root: { personalPages: [] } })
|
||||
const personalPages = useMemo(() => me?.root?.personalPages, [me?.root?.personalPages])
|
||||
const router = useRouter()
|
||||
const itemCount = personalPages?.length || 0
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(selectedPage: PersonalPage) => {
|
||||
@@ -31,24 +32,35 @@ export const PageList: React.FC<PageListProps> = ({ activeItemIndex, setActiveIt
|
||||
[router]
|
||||
)
|
||||
|
||||
const { listRef, setItemRef } = useKeyboardNavigation({
|
||||
personalPages,
|
||||
activeItemIndex,
|
||||
setActiveItemIndex,
|
||||
isCommandPaletteOpen,
|
||||
disableEnterKey,
|
||||
onEnter: handleEnter
|
||||
})
|
||||
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 && personalPages) {
|
||||
e.preventDefault()
|
||||
const selectedPage = personalPages[activeItemIndex]
|
||||
if (selectedPage) handleEnter?.(selectedPage)
|
||||
}
|
||||
},
|
||||
[itemCount, isCommandPaletteOpen, activeItemIndex, setActiveItemIndex, disableEnterKey, personalPages, handleEnter]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyDown)
|
||||
return () => window.removeEventListener("keydown", handleKeyDown)
|
||||
}, [handleKeyDown])
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden border-t">
|
||||
{!isTablet && <ColumnHeader />}
|
||||
<PageListItems
|
||||
listRef={listRef}
|
||||
setItemRef={setItemRef}
|
||||
personalPages={personalPages}
|
||||
activeItemIndex={activeItemIndex}
|
||||
/>
|
||||
<PageListItems personalPages={personalPages} activeItemIndex={activeItemIndex} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -72,29 +84,30 @@ export const ColumnHeader: React.FC = () => {
|
||||
}
|
||||
|
||||
interface PageListItemsProps {
|
||||
listRef: React.RefObject<HTMLDivElement>
|
||||
setItemRef: (el: HTMLAnchorElement | null, index: number) => void
|
||||
personalPages?: PersonalPageLists | null
|
||||
activeItemIndex: number | null
|
||||
}
|
||||
|
||||
const PageListItems: React.FC<PageListItemsProps> = ({ listRef, setItemRef, personalPages, activeItemIndex }) => (
|
||||
<Primitive.div
|
||||
ref={listRef}
|
||||
className="divide-primary/5 mx-auto my-2 flex w-[99%] 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: HTMLAnchorElement | null) => setItemRef(el, index)}
|
||||
page={page}
|
||||
isActive={index === activeItemIndex}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Primitive.div>
|
||||
)
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,14 +21,10 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ pa
|
||||
<Link
|
||||
ref={ref}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
className={cn(
|
||||
"relative block cursor-default rounded-lg outline-none",
|
||||
"h-12 items-center gap-x-2 py-2 max-lg:px-4 sm:px-6",
|
||||
{
|
||||
"bg-muted-foreground/10": isActive,
|
||||
"hover:bg-muted/50": !isActive
|
||||
}
|
||||
)}
|
||||
className={cn("relative block cursor-default outline-none", "min-h-12 py-2 max-lg:px-4 sm:px-6", {
|
||||
"bg-muted-foreground/5": isActive,
|
||||
"hover:bg-muted/50": !isActive
|
||||
})}
|
||||
href={`/pages/${page.id}`}
|
||||
role="listitem"
|
||||
>
|
||||
@@ -38,14 +34,9 @@ export const PageItem = React.forwardRef<HTMLAnchorElement, PageItemProps>(({ pa
|
||||
</Column.Wrapper>
|
||||
|
||||
{!isTablet && (
|
||||
<>
|
||||
{/* <Column.Wrapper style={columnStyles.content}>
|
||||
<Column.Text className="text-[13px]">{page.slug}</Column.Text>
|
||||
</Column.Wrapper> */}
|
||||
<Column.Wrapper style={columnStyles.topic}>
|
||||
{page.topic && <Badge variant="secondary">{page.topic.prettyName}</Badge>}
|
||||
</Column.Wrapper>
|
||||
</>
|
||||
<Column.Wrapper style={columnStyles.topic}>
|
||||
{page.topic && <Badge variant="secondary">{page.topic.prettyName}</Badge>}
|
||||
</Column.Wrapper>
|
||||
)}
|
||||
|
||||
<Column.Wrapper style={columnStyles.updated} className="flex justify-end">
|
||||
|
||||
Reference in New Issue
Block a user