fix: Bug fixing & Enhancement (#161)

* chore: memoize sorted pages

* chore: make link size more precise

* fix(link): disable enter press on create mode

* fix(onboarding): move is base logic and use escape for single quote

* fix(page): on delete success redirect to pages

* fix(sntry): sentry client error report

* chore(page): dynamic focus on title/content

* chore(link): tweak badge class

* chore(link): use nuqs for handling create mode

* fix(link): refs

* feat(palette): implement new link
This commit is contained in:
Aslam
2024-09-11 15:25:21 +07:00
committed by GitHub
parent 0668dd5625
commit 2a637705f2
20 changed files with 181 additions and 231 deletions

View File

@@ -13,6 +13,8 @@ NEXT_PUBLIC_JAZZ_PEER_URL="wss://"
RONIN_TOKEN=
SENTRY_DSN=
SENTRY_PROJECT=
NEXT_PUBLIC_SENTRY_DSN=
NEXT_PUBLIC_SENTRY_ORG=
NEXT_PUBLIC_SENTRY_PROJECT=
# IGNORE_BUILD_ERRORS=true

View File

@@ -1,9 +0,0 @@
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
// A faulty API route to test Sentry's error monitoring
export function GET() {
throw new Error("Sentry Example API Route Error");
return NextResponse.json({ data: "Testing Sentry Error..." });
}

View File

@@ -1,79 +0,0 @@
"use client";
import Head from "next/head";
import * as Sentry from "@sentry/nextjs";
export default function Page() {
return (
<div>
<Head>
<title>Sentry Onboarding</title>
<meta name="description" content="Test Sentry for your Next.js app!" />
</Head>
<main
style={{
minHeight: "100vh",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
}}
>
<h1 style={{ fontSize: "4rem", margin: "14px 0" }}>
<svg
style={{
height: "1em",
}}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 44"
>
<path
fill="currentColor"
d="M124.32,28.28,109.56,9.22h-3.68V34.77h3.73V15.19l15.18,19.58h3.26V9.22h-3.73ZM87.15,23.54h13.23V20.22H87.14V12.53h14.93V9.21H83.34V34.77h18.92V31.45H87.14ZM71.59,20.3h0C66.44,19.06,65,18.08,65,15.7c0-2.14,1.89-3.59,4.71-3.59a12.06,12.06,0,0,1,7.07,2.55l2-2.83a14.1,14.1,0,0,0-9-3c-5.06,0-8.59,3-8.59,7.27,0,4.6,3,6.19,8.46,7.52C74.51,24.74,76,25.78,76,28.11s-2,3.77-5.09,3.77a12.34,12.34,0,0,1-8.3-3.26l-2.25,2.69a15.94,15.94,0,0,0,10.42,3.85c5.48,0,9-2.95,9-7.51C79.75,23.79,77.47,21.72,71.59,20.3ZM195.7,9.22l-7.69,12-7.64-12h-4.46L186,24.67V34.78h3.84V24.55L200,9.22Zm-64.63,3.46h8.37v22.1h3.84V12.68h8.37V9.22H131.08ZM169.41,24.8c3.86-1.07,6-3.77,6-7.63,0-4.91-3.59-8-9.38-8H154.67V34.76h3.8V25.58h6.45l6.48,9.2h4.44l-7-9.82Zm-10.95-2.5V12.6h7.17c3.74,0,5.88,1.77,5.88,4.84s-2.29,4.86-5.84,4.86Z M29,2.26a4.67,4.67,0,0,0-8,0L14.42,13.53A32.21,32.21,0,0,1,32.17,40.19H27.55A27.68,27.68,0,0,0,12.09,17.47L6,28a15.92,15.92,0,0,1,9.23,12.17H4.62A.76.76,0,0,1,4,39.06l2.94-5a10.74,10.74,0,0,0-3.36-1.9l-2.91,5a4.54,4.54,0,0,0,1.69,6.24A4.66,4.66,0,0,0,4.62,44H19.15a19.4,19.4,0,0,0-8-17.31l2.31-4A23.87,23.87,0,0,1,23.76,44H36.07a35.88,35.88,0,0,0-16.41-31.8l4.67-8a.77.77,0,0,1,1.05-.27c.53.29,20.29,34.77,20.66,35.17a.76.76,0,0,1-.68,1.13H40.6q.09,1.91,0,3.81h4.78A4.59,4.59,0,0,0,50,39.43a4.49,4.49,0,0,0-.62-2.28Z"
></path>
</svg>
</h1>
<p>Get started by sending us a sample error:</p>
<button
type="button"
style={{
padding: "12px",
cursor: "pointer",
backgroundColor: "#AD6CAA",
borderRadius: "4px",
border: "none",
color: "white",
fontSize: "14px",
margin: "18px",
}}
onClick={async () => {
await Sentry.startSpan({
name: 'Example Frontend Span',
op: 'test'
}, async () => {
const res = await fetch("/api/sentry-example-api");
if (!res.ok) {
throw new Error("Sentry Example Frontend Error");
}
});
}}
>
Throw error!
</button>
<p>
Next, look for the error on the{" "}
<a href="https://sentry.io/organizations/omit-yci/issues/?project=4507928534712320">Issues Page</a>.
</p>
<p style={{ marginTop: "24px" }}>
For more information, see{" "}
<a href="https://docs.sentry.io/platforms/javascript/guides/nextjs/">
https://docs.sentry.io/platforms/javascript/guides/nextjs/
</a>
</p>
</main>
</div>
);
}

View File

@@ -71,7 +71,7 @@ export const createCommandGroups = (
icon: "Plus",
value: "Create New Link...",
label: "Create New Link...",
action: () => actions.navigateTo("/")
action: () => actions.navigateTo("/links?create=true")
}
]
},

View File

@@ -26,8 +26,6 @@ export function LearnAnythingOnboarding() {
const [isFetching, setIsFetching] = useState(true)
const [isExisting, setIsExisting] = useState(false)
if (pathname === "/") return null
useEffect(() => {
const loadUser = async () => {
try {
@@ -41,10 +39,10 @@ export function LearnAnythingOnboarding() {
}
}
if (!hasVisited) {
if (!hasVisited && pathname !== "/") {
loadUser()
}
}, [hasVisited, setIsOpen])
}, [hasVisited, pathname, setIsOpen])
const handleClose = () => {
setIsOpen(false)
@@ -68,8 +66,8 @@ export function LearnAnythingOnboarding() {
<p className="font-medium">Existing Customer Notice</p>
<p>
We noticed you are an existing Learn Anything customer. We sincerely apologize for any broken experience
you may have encountered on the old website. We've been working hard on this new version, which
addresses previous issues and offers more features. As an early customer, you're locked in at the{" "}
you may have encountered on the old website. We&apos;ve been working hard on this new version, which
addresses previous issues and offers more features. As an early customer, you&apos;re locked in at the{" "}
<strong>$3</strong> price for our upcoming pro version. Thank you for your support!
</p>
</>
@@ -85,8 +83,8 @@ export function LearnAnythingOnboarding() {
<li>Update your learning status on a topic</li>
</ul>
<p>
If you have any questions, don't hesitate to reach out. Click on question mark button in the bottom right
corner and enter your message.
If you have any questions, don&apos;t hesitate to reach out. Click on question mark button in the bottom
right corner and enter your message.
</p>
</AlertDialogDescription>

View File

@@ -1,24 +1,31 @@
import React from "react"
import Link from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { usePathname } from "next/navigation"
import { useAccount } from "@/lib/providers/jazz-provider"
import { cn } from "@/lib/utils"
import { PersonalLinkLists } from "@/lib/schema/personal-link"
import { useQueryState, parseAsStringLiteral } from "nuqs"
import { LEARNING_STATES } from "@/lib/constants"
export const LinkSection: React.FC<{ pathname: string }> = ({ pathname }) => {
const ALL_STATES = [{ label: "All", value: "all", icon: "List", className: "text-foreground" }, ...LEARNING_STATES]
const ALL_STATES_STRING = ALL_STATES.map(ls => ls.value)
interface LinkSectionProps {
pathname: string
}
export const LinkSection: React.FC<LinkSectionProps> = ({ pathname }) => {
const { me } = useAccount({
root: {
personalLinks: []
}
})
const linkCount = me?.root.personalLinks?.length || 0
const isActive = pathname === "/links"
if (!me) return null
const linkCount = me.root.personalLinks?.length || 0
const isActive = pathname === "/links"
return (
<div className="group/pages flex flex-col gap-px py-2">
<LinkSectionHeader linkCount={linkCount} isActive={isActive} />
@@ -34,20 +41,19 @@ interface LinkSectionHeaderProps {
const LinkSectionHeader: React.FC<LinkSectionHeaderProps> = ({ linkCount }) => {
const pathname = usePathname()
const [state] = useQueryState("state", parseAsStringLiteral(LEARNING_STATES.map(ls => ls.value)))
const isLinksActive = pathname.startsWith("/links") && !state
const [state] = useQueryState("state", parseAsStringLiteral(ALL_STATES_STRING))
const isLinksActive = pathname.startsWith("/links") && (!state || state === "all")
return (
<div className="flex gap-px rounded-md">
<div
className={cn(
"flex min-h-[30px] items-center gap-px rounded-md",
isLinksActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
)}
>
<Link
href="/links"
className={cn(
"flex size-6 flex-1 items-center justify-start rounded-md px-2",
"focus-visible:outline-none focus-visible:ring-0",
isLinksActive
? "bg-accent text-accent-foreground items-center justify-center py-3"
: "hover:bg-accent hover:text-accent-foreground"
)}
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">
Links
@@ -66,24 +72,29 @@ const List: React.FC<ListProps> = ({ personalLinks }) => {
const pathname = usePathname()
const [state] = useQueryState("state", parseAsStringLiteral(LEARNING_STATES.map(ls => ls.value)))
const toLearnCount = personalLinks.filter(link => link?.learningState === "wantToLearn").length
const learningCount = personalLinks.filter(link => link?.learningState === "learning").length
const learnedCount = personalLinks.filter(link => link?.learningState === "learned").length
const isActive = (checkState: string) => {
return pathname === "/links" && state === checkState
const linkCounts = {
wantToLearn: personalLinks.filter(link => link?.learningState === "wantToLearn").length,
learning: personalLinks.filter(link => link?.learningState === "learning").length,
learned: personalLinks.filter(link => link?.learningState === "learned").length
}
const isActive = (checkState: string) => pathname === "/links" && state === checkState
return (
<div className="flex flex-col gap-px">
<ListItem
label="To Learn"
href="/links?state=wantToLearn"
count={toLearnCount}
count={linkCounts.wantToLearn}
isActive={isActive("wantToLearn")}
/>
<ListItem label="Learning" href="/links?state=learning" count={learningCount} isActive={isActive("learning")} />
<ListItem label="Learned" href="/links?state=learned" count={learnedCount} isActive={isActive("learned")} />
<ListItem
label="Learning"
href="/links?state=learning"
count={linkCounts.learning}
isActive={isActive("learning")}
/>
<ListItem label="Learned" href="/links?state=learned" count={linkCounts.learned} isActive={isActive("learned")} />
</div>
)
}
@@ -95,26 +106,23 @@ interface ListItemProps {
isActive: boolean
}
const ListItem: React.FC<ListItemProps> = ({ label, href, count, isActive }) => {
return (
<div className="group/reorder-page relative">
<div className="group/topic-link relative flex min-w-0 flex-1">
<Link
href={href}
className={cn(
"relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium",
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
)}
>
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
<p className={cn("truncate opacity-95 group-hover/topic-link:opacity-100")}>{label}</p>
</div>
</Link>
{count > 0 && (
<span className="absolute right-2 top-1/2 z-[1] -translate-y-1/2 rounded p-1 text-sm">{count}</span>
const ListItem: React.FC<ListItemProps> = ({ label, href, count, isActive }) => (
<div className="group/reorder-page relative">
<div className="group/topic-link relative flex min-w-0 flex-1">
<Link
href={href}
className={cn(
"relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium",
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
)}
</div>
>
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
<p className={cn("truncate opacity-95 group-hover/topic-link:opacity-100")}>{label}</p>
</div>
</Link>
{count > 0 && (
<span className="absolute right-2 top-1/2 z-[1] -translate-y-1/2 rounded p-1 text-sm">{count}</span>
)}
</div>
)
}
</div>
)

View File

@@ -1,4 +1,4 @@
import React from "react"
import React, { useMemo } from "react"
import { useAtom } from "jotai"
import { usePathname, useRouter } from "next/navigation"
import { useAccount } from "@/lib/providers/jazz-provider"
@@ -9,7 +9,6 @@ import { Button } from "@/components/ui/button"
import { LaIcon } from "@/components/custom/la-icon"
import { toast } from "sonner"
import Link from "next/link"
import { useEffect } from "react"
import {
DropdownMenu,
DropdownMenuContent,
@@ -54,14 +53,14 @@ export const PageSection: React.FC<{ pathname?: string }> = ({ pathname }) => {
}
})
const [sort, setSort] = useAtom(pageSortAtom)
const [show, setShow] = useAtom(pageShowAtom)
const pageCount = me?.root.personalPages?.length || 0
const isActive = pathname === "/pages"
const [sort] = useAtom(pageSortAtom)
const [show] = useAtom(pageShowAtom)
if (!me) return null
const pageCount = me.root.personalPages?.length || 0
const isActive = pathname === "/pages"
return (
<div className="group/pages flex flex-col gap-px py-2">
<PageSectionHeader pageCount={pageCount} isActive={isActive} />
@@ -142,24 +141,19 @@ interface PageListProps {
show: ShowOption
}
const PageList: React.FC<PageListProps> = ({ personalPages }) => {
const PageList: React.FC<PageListProps> = ({ personalPages, sort, show }) => {
const pathname = usePathname()
const [sortCriteria] = useAtom(pageSortAtom)
const [showCount] = useAtom(pageShowAtom)
const sortedPages = [...personalPages]
.sort((a, b) => {
switch (sortCriteria) {
case "title":
const sortedPages = useMemo(() => {
return [...personalPages]
.sort((a, b) => {
if (sort === "title") {
return (a?.title ?? "").localeCompare(b?.title ?? "")
case "recent":
return (b?.updatedAt?.getTime() ?? 0) - (a?.updatedAt?.getTime() ?? 0)
default:
return 0
}
})
.slice(0, showCount === 0 ? personalPages.length : showCount)
}
return (b?.updatedAt?.getTime() ?? 0) - (a?.updatedAt?.getTime() ?? 0)
})
.slice(0, show === 0 ? personalPages.length : show)
}, [personalPages, sort, show])
return (
<div className="flex flex-col gap-px">
@@ -185,7 +179,7 @@ const PageListItem: React.FC<PageListItemProps> = ({ page, isActive }) => (
{ "bg-accent text-accent-foreground": isActive }
)}
>
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
<div className="flex max-w-[calc(100%-1rem)] flex-1 items-center gap-1.5 truncate text-sm">
<LaIcon name="FileText" className="flex-shrink-0 opacity-60" />
<p className="truncate opacity-95 group-hover/sidebar-link:opacity-100">{page.title || "Untitled"}</p>
</div>

View File

@@ -4,7 +4,7 @@ import React, { useEffect, useState, useCallback, useRef } from "react"
import { LinkHeader } from "@/components/routes/link/header"
import { LinkList } from "@/components/routes/link/list"
import { LinkManage } from "@/components/routes/link/manage"
import { useQueryState } from "nuqs"
import { parseAsBoolean, useQueryState } from "nuqs"
import { atom, useAtom } from "jotai"
import { LinkBottomBar } from "./bottom-bar"
import { commandPaletteOpenAtom } from "@/components/custom/command-palette/command-palette"
@@ -14,6 +14,7 @@ export const isDeleteConfirmShownAtom = atom(false)
export function LinkRoute(): React.ReactElement {
const [nuqsEditId] = useQueryState("editId")
const [activeItemIndex, setActiveItemIndex] = useState<number | null>(null)
const [isInCreateMode] = useQueryState("create", parseAsBoolean)
const [isCommandPaletteOpen] = useAtom(commandPaletteOpenAtom)
const [isDeleteConfirmShown] = useAtom(isDeleteConfirmShownAtom)
const [disableEnterKey, setDisableEnterKey] = useState(false)
@@ -32,7 +33,7 @@ export function LinkRoute(): React.ReactElement {
}, [])
useEffect(() => {
if (isDeleteConfirmShown || isCommandPaletteOpen) {
if (isDeleteConfirmShown || isCommandPaletteOpen || isInCreateMode) {
setDisableEnterKey(true)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
@@ -47,7 +48,7 @@ export function LinkRoute(): React.ReactElement {
clearTimeout(timeoutRef.current)
}
}
}, [isDeleteConfirmShown, isCommandPaletteOpen, handleCommandPaletteClose])
}, [isDeleteConfirmShown, isCommandPaletteOpen, isInCreateMode, handleCommandPaletteClose])
return (
<div className="flex h-full flex-auto flex-col overflow-hidden">

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react"
import React, { useCallback, useEffect, useRef } from "react"
import { motion, AnimatePresence } from "framer-motion"
import { icons } from "lucide-react"
import { Button } from "@/components/ui/button"
@@ -6,8 +6,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { getSpecialShortcut, formatShortcut, isMacOS } from "@/lib/utils"
import { LaIcon } from "@/components/custom/la-icon"
import { useAtom } from "jotai"
import { linkShowCreateAtom } from "@/store/link"
import { useQueryState } from "nuqs"
import { parseAsBoolean, useQueryState } from "nuqs"
import { useConfirm } from "@omit/react-confirm-dialog"
import { useAccount, useCoState } from "@/lib/providers/jazz-provider"
import { PersonalLink } from "@/lib/schema"
@@ -48,9 +47,8 @@ ToolbarButton.displayName = "ToolbarButton"
export const LinkBottomBar: React.FC = () => {
const [editId, setEditId] = useQueryState("editId")
const [createMode, setCreateMode] = useQueryState("create", parseAsBoolean)
const [, setGlobalLinkFormExceptionRefsAtom] = useAtom(globalLinkFormExceptionRefsAtom)
const [showCreate, setShowCreate] = useAtom(linkShowCreateAtom)
const { me } = useAccount({ root: { personalLinks: [] } })
const personalLink = useCoState(PersonalLink, editId as ID<PersonalLink>)
@@ -67,6 +65,13 @@ export const LinkBottomBar: React.FC = () => {
const { deleteLink } = useLinkActions()
const confirm = useConfirm()
const handleCreateMode = useCallback(() => {
setEditId(null)
setTimeout(() => {
setCreateMode(prev => !prev)
}, 100)
}, [setEditId, setCreateMode])
useEffect(() => {
setGlobalLinkFormExceptionRefsAtom([
overlayRef,
@@ -81,7 +86,7 @@ export const LinkBottomBar: React.FC = () => {
}, [setGlobalLinkFormExceptionRefsAtom])
const handleDelete = async (e: React.MouseEvent) => {
if (!personalLink) return
if (!personalLink || !me) return
const result = await confirm({
title: `Delete "${personalLink.title}"?`,
@@ -106,7 +111,6 @@ export const LinkBottomBar: React.FC = () => {
})
if (result) {
if (!me) return
deleteLink(me, personalLink)
setEditId(null)
}
@@ -114,24 +118,19 @@ export const LinkBottomBar: React.FC = () => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (isMacOS()) {
if (event.ctrlKey && event.metaKey && event.key.toLowerCase() === "n") {
event.preventDefault()
setShowCreate(true)
}
} else {
// For Windows, we'll use Ctrl + Win + N
// Note: The Windows key is not directly detectable in most browsers
if (event.ctrlKey && event.key.toLowerCase() === "n" && (event.metaKey || event.altKey)) {
event.preventDefault()
setShowCreate(true)
}
const isCreateShortcut = isMacOS()
? event.ctrlKey && event.metaKey && event.key.toLowerCase() === "n"
: event.ctrlKey && event.key.toLowerCase() === "n" && (event.metaKey || event.altKey)
if (isCreateShortcut) {
event.preventDefault()
handleCreateMode()
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [setShowCreate])
}, [handleCreateMode])
const shortcutKeys = getSpecialShortcut("expandToolbar")
const shortcutText = formatShortcut(shortcutKeys)
@@ -172,11 +171,11 @@ export const LinkBottomBar: React.FC = () => {
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.1 }}
>
{showCreate && <ToolbarButton icon={"ArrowLeft"} onClick={() => setShowCreate(true)} />}
{!showCreate && (
{createMode && <ToolbarButton icon={"ArrowLeft"} onClick={handleCreateMode} />}
{!createMode && (
<ToolbarButton
icon={"Plus"}
onClick={() => setShowCreate(true)}
onClick={handleCreateMode}
tooltip={`New Link (${shortcutText})`}
ref={plusBtnRef}
/>

View File

@@ -166,13 +166,11 @@ const LinkList: React.FC<LinkListProps> = ({ activeItemIndex, setActiveItemIndex
return newIndex
})
} else if (e.key === "Enter" && !disableEnterKey) {
} else if (e.key === "Enter" && !disableEnterKey && activeItemIndex !== null) {
e.preventDefault()
if (activeItemIndex !== null) {
const activeLink = sortedLinks[activeItemIndex]
if (activeLink) {
setEditId(activeLink.id)
}
const activeLink = sortedLinks[activeItemIndex]
if (activeLink) {
setEditId(activeLink.id)
}
}
}

View File

@@ -1,25 +1,24 @@
"use client"
import React from "react"
import { linkShowCreateAtom } from "@/store/link"
import { useAtom } from "jotai"
import { useKey } from "react-use"
import { LinkForm } from "./partials/form/link-form"
import { motion, AnimatePresence } from "framer-motion"
import { parseAsBoolean, useQueryState } from "nuqs"
interface LinkManageProps {}
const LinkManage: React.FC<LinkManageProps> = () => {
const [showCreate, setShowCreate] = useAtom(linkShowCreateAtom)
const [createMode, setCreateMode] = useQueryState("create", parseAsBoolean)
const handleFormClose = () => setShowCreate(false)
const handleFormClose = () => setCreateMode(false)
const handleFormFail = () => {}
useKey("Escape", handleFormClose)
return (
<AnimatePresence>
{showCreate && (
{createMode && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}

View File

@@ -83,7 +83,7 @@ export const LinkItem: React.FC<LinkItemProps> = ({
"relative cursor-default outline-none",
"grid grid-cols-[auto_1fr_auto] items-center gap-x-2 py-2 max-lg:px-4 sm:px-5 sm:py-2",
{
"bg-muted-foreground/10": isActive,
"bg-muted-foreground/5": isActive,
"hover:bg-muted/50": !isActive
}
)}
@@ -148,7 +148,11 @@ export const LinkItem: React.FC<LinkItemProps> = ({
</div>
<div className="flex shrink-0 items-center justify-end">
{personalLink.topic && <Badge variant="secondary">{personalLink.topic.prettyName}</Badge>}
{personalLink.topic && (
<Badge variant="secondary" className="border-muted-foreground/25">
{personalLink.topic.prettyName}
</Badge>
)}
</div>
</li>
)

View File

@@ -18,8 +18,8 @@ import { TopicSelector } from "@/components/custom/topic-selector"
import { Button } from "@/components/ui/button"
import { LaIcon } from "@/components/custom/la-icon"
import { useConfirm } from "@omit/react-confirm-dialog"
import { toast } from "sonner"
import { useRouter } from "next/navigation"
import { usePageActions } from "../hooks/use-page-actions"
const TITLE_PLACEHOLDER = "Untitled"
@@ -59,7 +59,9 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
const isMobile = useMedia("(max-width: 770px)")
const page = useCoState(PersonalPage, pageId as ID<PersonalPage>)
const router = useRouter()
const { deletePage } = usePageActions()
const confirm = useConfirm()
DeleteEmptyPage(pageId)
const handleDelete = async () => {
@@ -73,19 +75,8 @@ export function PageDetailRoute({ pageId }: { pageId: string }) {
})
if (result && me?.root.personalPages) {
try {
const index = me.root.personalPages.findIndex(item => item?.id === pageId)
if (index === -1) {
toast.error("Page not found.")
return
}
me.root.personalPages.splice(index, 1)
toast.success("Page deleted.", { position: "bottom-right" })
router.replace("/")
} catch (error) {
console.error("Delete operation fail", { error })
}
deletePage(me, pageId as ID<PersonalPage>)
router.push("/pages")
}
}
@@ -210,7 +201,7 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
const titleEditor = useEditor({
immediatelyRender: false,
autofocus: true,
autofocus: false,
extensions: [
FocusClasses,
Paragraph,
@@ -254,7 +245,13 @@ const DetailPageForm = ({ page }: { page: PersonalPage }) => {
useEffect(() => {
isTitleInitialMount.current = true
isContentInitialMount.current = true
}, [])
if (!page.title) {
titleEditor?.commands.focus()
} else {
contentEditorRef.current?.editor?.commands.focus()
}
}, [page.title, titleEditor, contentEditorRef])
return (
<div className="relative flex grow flex-col overflow-y-auto [scrollbar-gutter:stable]">

View File

@@ -0,0 +1,36 @@
import { useCallback } from "react"
import { toast } from "sonner"
import { LaAccount, PersonalPage } from "@/lib/schema"
import { ID } from "jazz-tools"
export const usePageActions = () => {
const deletePage = useCallback((me: LaAccount, pageId: ID<PersonalPage>): void => {
if (!me.root?.personalPages) return
const index = me.root.personalPages.findIndex(item => item?.id === pageId)
if (index === -1) {
toast.error("Page not found")
return
}
const page = me.root.personalPages[index]
if (!page) {
toast.error("Page data is invalid")
return
}
try {
me.root.personalPages.splice(index, 1)
toast.success("Page deleted", {
position: "bottom-right",
description: `${page.title} has been deleted.`
})
} catch (error) {
console.error("Failed to delete page", error)
toast.error("Failed to delete page")
}
}, [])
return { deletePage }
}

View File

@@ -1,5 +1,9 @@
import * as Sentry from "@sentry/nextjs"
export async function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config")
}
}
export const onRequestError = Sentry.captureRequestError

View File

@@ -46,9 +46,8 @@ export default withSentryConfig(nextConfig, {
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "learn-anything",
project: process.env.SENTRY_PROJECT,
sentryUrl: "https://sentry.io/",
org: process.env.NEXT_PUBLIC_SENTRY_ORG,
project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
@@ -64,7 +63,7 @@ export default withSentryConfig(nextConfig, {
enabled: true
},
// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.

View File

@@ -36,7 +36,7 @@
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@sentry/nextjs": "^8.29.0",
"@sentry/nextjs": "^8.30.0",
"@tanstack/react-virtual": "^3.10.7",
"@tiptap/core": "^2.6.6",
"@tiptap/extension-blockquote": "^2.6.6",
@@ -105,7 +105,7 @@
"zsa-react": "^0.2.2"
},
"devDependencies": {
"@ronin/learn-anything": "^0.0.0-3451915138150",
"@ronin/learn-anything": "^0.0.0-3451954511456",
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^16.0.1",
"@types/jest": "^29.5.12",

View File

@@ -5,7 +5,7 @@
import * as Sentry from "@sentry/nextjs"
Sentry.init({
dsn: process.env.SENTRY_DSN,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Add optional integrations for additional features
integrations: [Sentry.replayIntegration()],

View File

@@ -5,7 +5,7 @@
import * as Sentry from "@sentry/nextjs"
Sentry.init({
dsn: process.env.SENTRY_DSN,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
tracesSampleRate: 1,

View File

@@ -2,7 +2,6 @@ import { atom } from "jotai"
import { atomWithStorage } from "jotai/utils"
export const linkSortAtom = atomWithStorage("sort", "manual")
export const linkShowCreateAtom = atom(false)
export const linkEditIdAtom = atom<string | null>(null)
export const linkLearningStateSelectorAtom = atom(false)
export const linkOpenPopoverForIdAtom = atom<string | null>(null)