mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
chore: preload nested deps
This commit is contained in:
@@ -14,8 +14,6 @@ export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
|
|||||||
select: (state) => state.id === rootRouteId,
|
select: (state) => state.id === rootRouteId,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.error(error)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 p-4">
|
<div className="flex min-w-0 flex-1 flex-col items-center justify-center gap-6 p-4">
|
||||||
<ErrorComponent error={error} />
|
<ErrorComponent error={error} />
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export function KeyboardHandlerContent() {
|
|||||||
const { signOut } = useAuth()
|
const { signOut } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { me } = useAccountOrGuest()
|
const { me } = useAccountOrGuest()
|
||||||
const { newPage } = usePageActions()
|
const { createNewPage } = usePageActions()
|
||||||
|
|
||||||
const resetSequence = React.useCallback(() => {
|
const resetSequence = React.useCallback(() => {
|
||||||
setSequence([])
|
setSequence([])
|
||||||
@@ -81,9 +81,9 @@ export function KeyboardHandlerContent() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newPage()
|
createNewPage()
|
||||||
},
|
},
|
||||||
[newPage],
|
[createNewPage],
|
||||||
)
|
)
|
||||||
|
|
||||||
useKeyDown(
|
useKeyDown(
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ import { useAccount } from "@/lib/providers/jazz-provider"
|
|||||||
import { NavItem } from "~/components/custom/nav-item"
|
import { NavItem } from "~/components/custom/nav-item"
|
||||||
|
|
||||||
export const LinkCollection: React.FC = () => {
|
export const LinkCollection: React.FC = () => {
|
||||||
const { me } = useAccount()
|
const { me } = useAccount({
|
||||||
|
root: {
|
||||||
|
personalLinks: [{}],
|
||||||
|
personalPages: [{}],
|
||||||
|
tasks: [{}],
|
||||||
|
topicsWantToLearn: [{}],
|
||||||
|
topicsLearning: [{}],
|
||||||
|
topicsLearned: [{}],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const linkCount = me?.root?.personalLinks?.length || 0
|
const linkCount = me?.root?.personalLinks?.length || 0
|
||||||
const pageCount = me?.root?.personalPages?.length || 0
|
const pageCount = me?.root?.personalPages?.length || 0
|
||||||
|
|||||||
@@ -49,11 +49,7 @@ const isExpandedAtom = atomWithStorage("isPageSectionExpanded", true)
|
|||||||
export const PageSection: React.FC = () => {
|
export const PageSection: React.FC = () => {
|
||||||
const { me } = useAccount({
|
const { me } = useAccount({
|
||||||
root: {
|
root: {
|
||||||
personalPages: [
|
personalPages: [{}],
|
||||||
{
|
|
||||||
topic: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -81,9 +77,9 @@ export const PageSection: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="flex flex-col gap-px">
|
<div className="flex flex-col gap-px">
|
||||||
{sortedPages.map((page) => (
|
{sortedPages.map((page, index) => (
|
||||||
<Link
|
<Link
|
||||||
key={page.id}
|
key={index}
|
||||||
to="/pages/$pageId"
|
to="/pages/$pageId"
|
||||||
params={{ pageId: page.id }}
|
params={{ pageId: page.id }}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -168,13 +164,13 @@ const PageSectionHeader: React.FC<PageSectionHeaderProps> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const NewPageButton: React.FC = () => {
|
const NewPageButton: React.FC = () => {
|
||||||
const { newPage } = usePageActions()
|
const { createNewPage } = usePageActions()
|
||||||
|
|
||||||
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
newPage()
|
createNewPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,22 +19,16 @@ import { SignInButton, useAuth, useUser } from "@clerk/tanstack-start"
|
|||||||
import { Link, useLocation } from "@tanstack/react-router"
|
import { Link, useLocation } from "@tanstack/react-router"
|
||||||
import { ShortcutKey } from "@shared/minimal-tiptap/components/shortcut-key"
|
import { ShortcutKey } from "@shared/minimal-tiptap/components/shortcut-key"
|
||||||
import { Feedback } from "./feedback"
|
import { Feedback } from "./feedback"
|
||||||
|
import { useAccount } from "~/lib/providers/jazz-provider"
|
||||||
|
|
||||||
export const ProfileSection: React.FC = () => {
|
export const ProfileSection: React.FC = () => {
|
||||||
const { user, isSignedIn } = useUser()
|
const { user, isSignedIn } = useUser()
|
||||||
const { signOut } = useAuth()
|
|
||||||
const [menuOpen, setMenuOpen] = React.useState(false)
|
const [menuOpen, setMenuOpen] = React.useState(false)
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const [, setShowShortcut] = useAtom(showShortcutAtom)
|
const [, setShowShortcut] = useAtom(showShortcutAtom)
|
||||||
|
|
||||||
const { disableKeydown } = useKeyboardManager("profileSection")
|
const { disableKeydown } = useKeyboardManager("profileSection")
|
||||||
|
|
||||||
const handleSignOut = () => {
|
|
||||||
signOut(() => {
|
|
||||||
window.location.replace("/")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
disableKeydown(menuOpen)
|
disableKeydown(menuOpen)
|
||||||
}, [menuOpen, disableKeydown])
|
}, [menuOpen, disableKeydown])
|
||||||
@@ -59,7 +53,6 @@ export const ProfileSection: React.FC = () => {
|
|||||||
user={user}
|
user={user}
|
||||||
menuOpen={menuOpen}
|
menuOpen={menuOpen}
|
||||||
setMenuOpen={setMenuOpen}
|
setMenuOpen={setMenuOpen}
|
||||||
signOut={handleSignOut}
|
|
||||||
setShowShortcut={setShowShortcut}
|
setShowShortcut={setShowShortcut}
|
||||||
/>
|
/>
|
||||||
<span className="flex flex-auto"></span>
|
<span className="flex flex-auto"></span>
|
||||||
@@ -73,7 +66,6 @@ interface ProfileDropdownProps {
|
|||||||
user: any
|
user: any
|
||||||
menuOpen: boolean
|
menuOpen: boolean
|
||||||
setMenuOpen: (open: boolean) => void
|
setMenuOpen: (open: boolean) => void
|
||||||
signOut: () => void
|
|
||||||
setShowShortcut: (show: boolean) => void
|
setShowShortcut: (show: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +73,6 @@ const ProfileDropdown: React.FC<ProfileDropdownProps> = ({
|
|||||||
user,
|
user,
|
||||||
menuOpen,
|
menuOpen,
|
||||||
setMenuOpen,
|
setMenuOpen,
|
||||||
signOut,
|
|
||||||
setShowShortcut,
|
setShowShortcut,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="flex min-w-0">
|
<div className="flex min-w-0">
|
||||||
@@ -107,60 +98,71 @@ const ProfileDropdown: React.FC<ProfileDropdownProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent className="w-56" align="start" side="top">
|
<DropdownMenuContent className="w-56" align="start" side="top">
|
||||||
<DropdownMenuItems
|
<DropdownMenuItems setShowShortcut={setShowShortcut} />
|
||||||
signOut={signOut}
|
|
||||||
setShowShortcut={setShowShortcut}
|
|
||||||
/>
|
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
interface DropdownMenuItemsProps {
|
interface DropdownMenuItemsProps {
|
||||||
signOut: () => void
|
|
||||||
setShowShortcut: (show: boolean) => void
|
setShowShortcut: (show: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuItems: React.FC<DropdownMenuItemsProps> = ({
|
const DropdownMenuItems: React.FC<DropdownMenuItemsProps> = ({
|
||||||
signOut,
|
|
||||||
setShowShortcut,
|
setShowShortcut,
|
||||||
}) => (
|
}) => {
|
||||||
<>
|
const { signOut } = useAuth()
|
||||||
<MenuLink href="/profile" icon="CircleUser" text="My profile" />
|
const { logOut } = useAccount()
|
||||||
<DropdownMenuItem className="gap-2" onClick={() => setShowShortcut(true)}>
|
|
||||||
<LaIcon name="Keyboard" />
|
const handleSignOut = React.useCallback(async () => {
|
||||||
<span>Shortcut</span>
|
try {
|
||||||
</DropdownMenuItem>
|
logOut()
|
||||||
<MenuLink href="/onboarding" icon="LayoutList" text="Onboarding" />
|
signOut(() => {
|
||||||
<DropdownMenuSeparator />
|
window.location.href = "/"
|
||||||
<MenuLink
|
})
|
||||||
href="https://docs.learn-anything.xyz/"
|
} catch (error) {
|
||||||
icon="Sticker"
|
console.error("Error signing out:", error)
|
||||||
text="Docs"
|
}
|
||||||
/>
|
}, [logOut, signOut])
|
||||||
<MenuLink
|
|
||||||
href="https://github.com/learn-anything/learn-anything"
|
return (
|
||||||
icon="Github"
|
<>
|
||||||
text="GitHub"
|
<MenuLink href="/profile" icon="CircleUser" text="My profile" />
|
||||||
/>
|
<DropdownMenuItem className="gap-2" onClick={() => setShowShortcut(true)}>
|
||||||
<MenuLink
|
<LaIcon name="Keyboard" />
|
||||||
href="https://discord.com/invite/bxtD8x6aNF"
|
<span>Shortcut</span>
|
||||||
icon={DiscordIcon}
|
</DropdownMenuItem>
|
||||||
text="Discord"
|
<MenuLink href="/onboarding" icon="LayoutList" text="Onboarding" />
|
||||||
iconClass="-ml-1"
|
<DropdownMenuSeparator />
|
||||||
/>
|
<MenuLink
|
||||||
<DropdownMenuSeparator />
|
href="https://docs.learn-anything.xyz/"
|
||||||
<DropdownMenuItem onClick={signOut}>
|
icon="Sticker"
|
||||||
<div className="relative flex flex-1 cursor-pointer items-center gap-2">
|
text="Docs"
|
||||||
<LaIcon name="LogOut" />
|
/>
|
||||||
<span>Log out</span>
|
<MenuLink
|
||||||
<div className="absolute right-0">
|
href="https://github.com/learn-anything/learn-anything"
|
||||||
<ShortcutKey keys={["alt", "shift", "q"]} />
|
icon="Github"
|
||||||
|
text="GitHub"
|
||||||
|
/>
|
||||||
|
<MenuLink
|
||||||
|
href="https://discord.com/invite/bxtD8x6aNF"
|
||||||
|
icon={DiscordIcon}
|
||||||
|
text="Discord"
|
||||||
|
iconClass="-ml-1"
|
||||||
|
/>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem onClick={handleSignOut}>
|
||||||
|
<div className="relative flex flex-1 cursor-pointer items-center gap-2">
|
||||||
|
<LaIcon name="LogOut" />
|
||||||
|
<span>Log out</span>
|
||||||
|
<div className="absolute right-0">
|
||||||
|
<ShortcutKey keys={["alt", "shift", "q"]} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
</>
|
||||||
</>
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
interface MenuLinkProps {
|
interface MenuLinkProps {
|
||||||
href: string
|
href: string
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useNavigate } from "@tanstack/react-router"
|
|||||||
export const useCommandActions = () => {
|
export const useCommandActions = () => {
|
||||||
const { setTheme } = useTheme()
|
const { setTheme } = useTheme()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { newPage } = usePageActions()
|
const { createNewPage } = usePageActions()
|
||||||
|
|
||||||
const changeTheme = React.useCallback(
|
const changeTheme = React.useCallback(
|
||||||
(theme: string) => {
|
(theme: string) => {
|
||||||
@@ -39,6 +39,6 @@ export const useCommandActions = () => {
|
|||||||
navigateTo,
|
navigateTo,
|
||||||
openLinkInNewTab,
|
openLinkInNewTab,
|
||||||
copyCurrentURL,
|
copyCurrentURL,
|
||||||
createNewPage: newPage,
|
createNewPage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,60 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
import { LaAccount, PersonalPage } from "@/lib/schema"
|
import { PersonalPage } from "@/lib/schema"
|
||||||
import { ID } from "jazz-tools"
|
import { ID } from "jazz-tools"
|
||||||
import { useNavigate } from "@tanstack/react-router"
|
import { useNavigate } from "@tanstack/react-router"
|
||||||
import { useAccountOrGuest } from "~/lib/providers/jazz-provider"
|
import { useAccountOrGuest } from "~/lib/providers/jazz-provider"
|
||||||
|
|
||||||
export const usePageActions = () => {
|
export const usePageActions = () => {
|
||||||
const { me } = useAccountOrGuest()
|
const { me: account } = useAccountOrGuest()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const newPage = React.useCallback(() => {
|
const createNewPage = React.useCallback(async () => {
|
||||||
if (!me) return
|
try {
|
||||||
if (me._type !== "Account") return
|
const isValidAccount = account && account._type === "Account"
|
||||||
|
if (!isValidAccount) return
|
||||||
|
|
||||||
const page = PersonalPage.create(
|
const page = PersonalPage.create(
|
||||||
{ public: false, createdAt: new Date(), updatedAt: new Date() },
|
{
|
||||||
{ owner: me },
|
public: false,
|
||||||
)
|
topic: null,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
{ owner: account },
|
||||||
|
)
|
||||||
|
|
||||||
me.root?.personalPages?.push(page)
|
account.root?.personalPages?.push(page)
|
||||||
|
|
||||||
navigate({ to: "/pages/$pageId", params: { pageId: page.id } })
|
navigate({
|
||||||
}, [me, navigate])
|
to: "/pages/$pageId",
|
||||||
|
params: { pageId: page.id },
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}, [account, navigate])
|
||||||
|
|
||||||
const deletePage = React.useCallback(
|
const deletePage = React.useCallback(
|
||||||
(me: LaAccount, pageId: ID<PersonalPage>): void => {
|
(pageId: ID<PersonalPage>): void => {
|
||||||
if (!me.root?.personalPages) return
|
const isValidAccount = account && account._type === "Account"
|
||||||
|
if (!isValidAccount) return
|
||||||
|
|
||||||
const index = me.root.personalPages.findIndex(
|
const found = account.root?.personalPages?.findIndex(
|
||||||
(item) => item?.id === pageId,
|
(item) => item?.id === pageId,
|
||||||
)
|
)
|
||||||
if (index === -1) {
|
|
||||||
toast.error("Page not found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = me.root.personalPages[index]
|
if (found !== undefined && found > -1) {
|
||||||
if (!page) {
|
account.root?.personalPages?.splice(found, 1)
|
||||||
toast.error("Page data is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
me.root.personalPages.splice(index, 1)
|
|
||||||
|
|
||||||
toast.success("Page deleted", {
|
toast.success("Page deleted", {
|
||||||
position: "bottom-right",
|
description: "The page has been deleted",
|
||||||
description: `${page.title} has been deleted.`,
|
|
||||||
})
|
})
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to delete page", error)
|
|
||||||
toast.error("Failed to delete page")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[],
|
[account],
|
||||||
)
|
)
|
||||||
|
|
||||||
return { newPage, deletePage }
|
return { createNewPage, deletePage }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,17 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router"
|
|||||||
import { routeTree } from "./routeTree.gen"
|
import { routeTree } from "./routeTree.gen"
|
||||||
import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary"
|
import { DefaultCatchBoundary } from "./components/DefaultCatchBoundary"
|
||||||
import { NotFound } from "./components/NotFound"
|
import { NotFound } from "./components/NotFound"
|
||||||
import { QueryClient } from "@tanstack/react-query"
|
|
||||||
import { routerWithQueryClient } from "@tanstack/react-router-with-query"
|
|
||||||
|
|
||||||
export function createRouter() {
|
export function createRouter() {
|
||||||
const queryClient = new QueryClient()
|
const router = createTanStackRouter({
|
||||||
const router = routerWithQueryClient(
|
routeTree,
|
||||||
createTanStackRouter({
|
defaultPreload: "intent",
|
||||||
routeTree,
|
defaultErrorComponent: DefaultCatchBoundary,
|
||||||
defaultPreload: "intent",
|
defaultNotFoundComponent: () => <NotFound />,
|
||||||
defaultErrorComponent: DefaultCatchBoundary,
|
context: {
|
||||||
defaultNotFoundComponent: () => <NotFound />,
|
auth: undefined,
|
||||||
context: { queryClient, auth: undefined! },
|
},
|
||||||
}),
|
})
|
||||||
queryClient,
|
|
||||||
)
|
|
||||||
|
|
||||||
return router
|
return router
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
import { getAuth } from "@clerk/tanstack-start/server"
|
import { getAuth } from "@clerk/tanstack-start/server"
|
||||||
import type { QueryClient } from "@tanstack/react-query"
|
|
||||||
import {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
ScrollRestoration,
|
ScrollRestoration,
|
||||||
@@ -29,15 +28,6 @@ export const TanStackRouterDevtools =
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const ReactQueryDevtools =
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? () => null
|
|
||||||
: React.lazy(() =>
|
|
||||||
import("@tanstack/react-query-devtools").then((d) => ({
|
|
||||||
default: d.ReactQueryDevtools,
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
|
export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
|
||||||
const auth = await getAuth(ctx.request)
|
const auth = await getAuth(ctx.request)
|
||||||
|
|
||||||
@@ -45,8 +35,7 @@ export const fetchClerkAuth = createServerFn("GET", async (_, ctx) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{
|
export const Route = createRootRouteWithContext<{
|
||||||
auth: { userId: string }
|
auth?: ReturnType<typeof getAuth> | null
|
||||||
queryClient: QueryClient
|
|
||||||
}>()({
|
}>()({
|
||||||
meta: () => [
|
meta: () => [
|
||||||
{
|
{
|
||||||
@@ -86,13 +75,25 @@ export const Route = createRootRouteWithContext<{
|
|||||||
{ rel: "manifest", href: "/site.webmanifest", color: "#fffff" },
|
{ rel: "manifest", href: "/site.webmanifest", color: "#fffff" },
|
||||||
{ rel: "icon", href: "/favicon.ico" },
|
{ rel: "icon", href: "/favicon.ico" },
|
||||||
],
|
],
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async (ctx) => {
|
||||||
if (context.auth) {
|
try {
|
||||||
return { auth: context.auth }
|
// Handle explicit null auth (logged out state)
|
||||||
}
|
if (ctx.context.auth === null) {
|
||||||
|
return { auth: null }
|
||||||
|
}
|
||||||
|
|
||||||
const auth = await fetchClerkAuth()
|
// Use existing auth if available
|
||||||
return { auth }
|
if (ctx.context.auth) {
|
||||||
|
return { auth: ctx.context.auth }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch new auth state
|
||||||
|
const auth = await fetchClerkAuth()
|
||||||
|
return { auth }
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in beforeLoad:", error)
|
||||||
|
return { auth: null }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
errorComponent: (props) => {
|
errorComponent: (props) => {
|
||||||
return (
|
return (
|
||||||
@@ -101,6 +102,11 @@ export const Route = createRootRouteWithContext<{
|
|||||||
</RootDocument>
|
</RootDocument>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
pendingComponent: () => (
|
||||||
|
<RootDocument>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</RootDocument>
|
||||||
|
),
|
||||||
notFoundComponent: () => <NotFound />,
|
notFoundComponent: () => <NotFound />,
|
||||||
component: RootComponent,
|
component: RootComponent,
|
||||||
})
|
})
|
||||||
@@ -124,7 +130,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
<React.Suspense>
|
<React.Suspense>
|
||||||
<TanStackRouterDevtools position="bottom-right" />
|
<TanStackRouterDevtools position="bottom-right" />
|
||||||
<ReactQueryDevtools buttonPosition="bottom-right" />
|
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
|
|
||||||
<ScrollRestoration />
|
<ScrollRestoration />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const openPopoverForIdAtom = atom<string | null>(null)
|
|||||||
|
|
||||||
export function TopicDetailComponent() {
|
export function TopicDetailComponent() {
|
||||||
const params = useParams({ from: "/_layout/_pages/(topic)/$" })
|
const params = useParams({ from: "/_layout/_pages/(topic)/$" })
|
||||||
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
|
const { me } = useAccountOrGuest({ root: { personalLinks: [{}] } })
|
||||||
|
|
||||||
const topicID = React.useMemo(
|
const topicID = React.useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export const TopicDetailHeader = function TopicDetailHeader({
|
|||||||
const isMobile = useMedia("(max-width: 770px)")
|
const isMobile = useMedia("(max-width: 770px)")
|
||||||
const { me } = useAccountOrGuest({
|
const { me } = useAccountOrGuest({
|
||||||
root: {
|
root: {
|
||||||
topicsWantToLearn: [],
|
topicsWantToLearn: [{}],
|
||||||
topicsLearning: [],
|
topicsLearning: [{}],
|
||||||
topicsLearned: [],
|
topicsLearned: [{}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function TopicDetailList({
|
|||||||
activeIndex,
|
activeIndex,
|
||||||
setActiveIndex,
|
setActiveIndex,
|
||||||
}: TopicDetailListProps) {
|
}: TopicDetailListProps) {
|
||||||
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
|
const { me } = useAccountOrGuest({ root: { personalLinks: [{}] } })
|
||||||
const personalLinks =
|
const personalLinks =
|
||||||
!me || me._type === "Anonymous" ? undefined : me.root.personalLinks
|
!me || me._type === "Anonymous" ? undefined : me.root.personalLinks
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,24 @@ import { createFileRoute, Outlet, redirect } from "@tanstack/react-router"
|
|||||||
|
|
||||||
export const Route = createFileRoute("/_layout/_pages/_protected")({
|
export const Route = createFileRoute("/_layout/_pages/_protected")({
|
||||||
beforeLoad: async ({ context, location }) => {
|
beforeLoad: async ({ context, location }) => {
|
||||||
if (!context?.auth?.userId) {
|
// Add extra validation
|
||||||
|
if (!context || !context.auth) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/sign-in/$",
|
to: "/sign-in/$",
|
||||||
search: { redirect_url: location.pathname },
|
search: { redirect_url: location.pathname },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auth = await context.auth
|
||||||
|
|
||||||
|
if (!auth?.userId) {
|
||||||
|
throw redirect({
|
||||||
|
to: "/sign-in/$",
|
||||||
|
search: { redirect_url: location.pathname },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return context
|
||||||
},
|
},
|
||||||
component: () => <Outlet />,
|
component: () => <Outlet />,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ interface Question {
|
|||||||
|
|
||||||
function CommunityTopicComponent() {
|
function CommunityTopicComponent() {
|
||||||
const { topicName } = Route.useParams()
|
const { topicName } = Route.useParams()
|
||||||
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
|
const { me } = useAccountOrGuest()
|
||||||
const topicID = useMemo(
|
const topicID = useMemo(
|
||||||
() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me),
|
() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me),
|
||||||
[topicName, me],
|
[topicName, me],
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export const LinkForm: React.FC<LinkFormProps> = ({
|
|||||||
|
|
||||||
const [isFetching, setIsFetching] = React.useState(false)
|
const [isFetching, setIsFetching] = React.useState(false)
|
||||||
const [urlFetched, setUrlFetched] = React.useState<string | null>(null)
|
const [urlFetched, setUrlFetched] = React.useState<string | null>(null)
|
||||||
const { me } = useAccount()
|
const { me } = useAccount({ root: { personalLinks: [{}] } })
|
||||||
const selectedLink = useCoState(PersonalLink, personalLink?.id)
|
const selectedLink = useCoState(PersonalLink, personalLink?.id)
|
||||||
|
|
||||||
const form = useForm<LinkFormValues>({
|
const form = useForm<LinkFormValues>({
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ const StepItem = ({
|
|||||||
function OnboardingComponent() {
|
function OnboardingComponent() {
|
||||||
const { me } = useAccount({
|
const { me } = useAccount({
|
||||||
root: {
|
root: {
|
||||||
personalPages: [],
|
personalPages: [{}],
|
||||||
personalLinks: [],
|
personalLinks: [{}],
|
||||||
topicsWantToLearn: [],
|
topicsWantToLearn: [{}],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||||
import { ID } from "jazz-tools"
|
import { ID } from "jazz-tools"
|
||||||
import { LaAccount, PersonalPage } from "@/lib/schema"
|
import { PersonalPage } from "@/lib/schema"
|
||||||
import { Content, EditorContent, useEditor } from "@tiptap/react"
|
import { Content, EditorContent, useEditor } from "@tiptap/react"
|
||||||
import { useAccount, useCoState } from "@/lib/providers/jazz-provider"
|
import { useCoState } from "@/lib/providers/jazz-provider"
|
||||||
import { EditorView } from "@tiptap/pm/view"
|
import { EditorView } from "@tiptap/pm/view"
|
||||||
import { Editor } from "@tiptap/core"
|
import { Editor } from "@tiptap/core"
|
||||||
import { generateUniqueSlug } from "@/lib/utils"
|
import { generateUniqueSlug } from "@/lib/utils"
|
||||||
@@ -29,17 +29,10 @@ const TITLE_PLACEHOLDER = "Untitled"
|
|||||||
|
|
||||||
function PageDetailComponent() {
|
function PageDetailComponent() {
|
||||||
const { pageId } = Route.useParams()
|
const { pageId } = Route.useParams()
|
||||||
const { me } = useAccount({
|
|
||||||
root: {
|
|
||||||
personalPages: [
|
|
||||||
{
|
|
||||||
topic: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const isMobile = useMedia("(max-width: 770px)")
|
const isMobile = useMedia("(max-width: 770px)")
|
||||||
const page = useCoState(PersonalPage, pageId as ID<PersonalPage>)
|
const page = useCoState(PersonalPage, pageId as ID<PersonalPage>, {
|
||||||
|
topic: {},
|
||||||
|
})
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { deletePage } = usePageActions()
|
const { deletePage } = usePageActions()
|
||||||
@@ -55,13 +48,13 @@ function PageDetailComponent() {
|
|||||||
confirmButton: { variant: "destructive" },
|
confirmButton: { variant: "destructive" },
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result && me?.root.personalPages) {
|
if (result) {
|
||||||
deletePage(me, pageId as ID<PersonalPage>)
|
deletePage(pageId as ID<PersonalPage>)
|
||||||
navigate({ to: "/pages" })
|
navigate({ to: "/pages" })
|
||||||
}
|
}
|
||||||
}, [confirm, deletePage, me, pageId, navigate])
|
}, [confirm, deletePage, pageId, navigate])
|
||||||
|
|
||||||
if (!page || !me) return null
|
if (!page) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute inset-0 flex flex-row overflow-hidden">
|
<div className="absolute inset-0 flex flex-row overflow-hidden">
|
||||||
@@ -72,7 +65,7 @@ function PageDetailComponent() {
|
|||||||
handleDelete={handleDelete}
|
handleDelete={handleDelete}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
<DetailPageForm page={page} me={me} />
|
<DetailPageForm page={page} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
@@ -132,29 +125,11 @@ const SidebarActions = ({
|
|||||||
|
|
||||||
SidebarActions.displayName = "SidebarActions"
|
SidebarActions.displayName = "SidebarActions"
|
||||||
|
|
||||||
const DetailPageForm = ({
|
const DetailPageForm = ({ page }: { page: PersonalPage }) => {
|
||||||
page,
|
|
||||||
me,
|
|
||||||
}: {
|
|
||||||
page: PersonalPage
|
|
||||||
me: LaAccount
|
|
||||||
}) => {
|
|
||||||
const titleEditorRef = React.useRef<Editor | null>(null)
|
const titleEditorRef = React.useRef<Editor | null>(null)
|
||||||
const contentEditorRef = React.useRef<Editor | null>(null)
|
const contentEditorRef = React.useRef<Editor | null>(null)
|
||||||
const [isInitialSync, setIsInitialSync] = React.useState(true)
|
const [isInitialSync, setIsInitialSync] = React.useState(true)
|
||||||
|
|
||||||
// const { id: pageId, title: pageTitle } = page
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (!pageId) return
|
|
||||||
|
|
||||||
// if (!pageTitle && titleEditorRef.current) {
|
|
||||||
// titleEditorRef.current.commands.focus()
|
|
||||||
// } else if (contentEditorRef.current) {
|
|
||||||
// contentEditorRef.current.commands.focus()
|
|
||||||
// }
|
|
||||||
// }, [pageId, pageTitle])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!page) return
|
if (!page) return
|
||||||
|
|
||||||
@@ -293,17 +268,25 @@ const DetailPageForm = ({
|
|||||||
onUpdate: ({ editor }) => handleUpdateTitle(editor),
|
onUpdate: ({ editor }) => handleUpdateTitle(editor),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { content: pageContent, title: pageTitle } = page
|
||||||
|
|
||||||
const handleCreate = React.useCallback(
|
const handleCreate = React.useCallback(
|
||||||
({ editor }: { editor: Editor }) => {
|
({ editor }: { editor: Editor }) => {
|
||||||
contentEditorRef.current = editor
|
contentEditorRef.current = editor
|
||||||
|
|
||||||
if (page.content) {
|
if (pageContent) {
|
||||||
editor.commands.setContent(page.content as Content)
|
editor.commands.setContent(pageContent as Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsInitialSync(false)
|
setIsInitialSync(false)
|
||||||
|
|
||||||
|
if (!pageTitle && titleEditorRef.current) {
|
||||||
|
titleEditorRef.current.commands.focus()
|
||||||
|
} else if (contentEditorRef.current) {
|
||||||
|
contentEditorRef.current.commands.focus()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[page.content],
|
[pageContent, pageTitle],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -312,6 +295,7 @@ const DetailPageForm = ({
|
|||||||
<form className="flex shrink-0 flex-col">
|
<form className="flex shrink-0 flex-col">
|
||||||
<div className="mb-2 mt-8 py-1.5">
|
<div className="mb-2 mt-8 py-1.5">
|
||||||
<EditorContent
|
<EditorContent
|
||||||
|
key={page.id}
|
||||||
editor={titleEditor}
|
editor={titleEditor}
|
||||||
className="title-editor no-command grow cursor-text select-text text-2xl font-semibold leading-[calc(1.33333)] tracking-[-0.00625rem]"
|
className="title-editor no-command grow cursor-text select-text text-2xl font-semibold leading-[calc(1.33333)] tracking-[-0.00625rem]"
|
||||||
/>
|
/>
|
||||||
@@ -319,8 +303,7 @@ const DetailPageForm = ({
|
|||||||
<div className="flex flex-auto flex-col">
|
<div className="flex flex-auto flex-col">
|
||||||
<div className="relative flex h-full max-w-full grow flex-col items-stretch p-0">
|
<div className="relative flex h-full max-w-full grow flex-col items-stretch p-0">
|
||||||
<LaEditor
|
<LaEditor
|
||||||
me={me}
|
key={page.id}
|
||||||
personalPage={page}
|
|
||||||
editorClassName="-mx-3.5 px-3.5 py-2.5 flex-auto focus:outline-none"
|
editorClassName="-mx-3.5 px-3.5 py-2.5 flex-auto focus:outline-none"
|
||||||
value={page.content as Content}
|
value={page.content as Content}
|
||||||
placeholder="Add content..."
|
placeholder="Add content..."
|
||||||
|
|||||||
@@ -5,16 +5,12 @@ import {
|
|||||||
SidebarToggleButton,
|
SidebarToggleButton,
|
||||||
} from "@/components/custom/content-header"
|
} from "@/components/custom/content-header"
|
||||||
import { LaIcon } from "@/components/custom/la-icon"
|
import { LaIcon } from "@/components/custom/la-icon"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
|
||||||
import { usePageActions } from "~/hooks/actions/use-page-actions"
|
import { usePageActions } from "~/hooks/actions/use-page-actions"
|
||||||
|
|
||||||
interface PageHeaderProps {}
|
interface PageHeaderProps {}
|
||||||
|
|
||||||
export const PageHeader: React.FC<PageHeaderProps> = () => {
|
export const PageHeader: React.FC<PageHeaderProps> = () => {
|
||||||
const { me } = useAccount()
|
const { createNewPage } = usePageActions()
|
||||||
const { newPage } = usePageActions()
|
|
||||||
|
|
||||||
if (!me) return null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentHeader>
|
<ContentHeader>
|
||||||
@@ -28,7 +24,7 @@ export const PageHeader: React.FC<PageHeaderProps> = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="gap-x-2"
|
className="gap-x-2"
|
||||||
onClick={newPage}
|
onClick={createNewPage}
|
||||||
>
|
>
|
||||||
<LaIcon name="Plus" />
|
<LaIcon name="Plus" />
|
||||||
<span className="hidden md:block">New page</span>
|
<span className="hidden md:block">New page</span>
|
||||||
|
|||||||
@@ -26,7 +26,14 @@ const ProfileStats: React.FC<ProfileStatsProps> = ({ number, label }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ProfileComponent() {
|
function ProfileComponent() {
|
||||||
const account = useAccount()
|
const account = useAccount({
|
||||||
|
profile: {},
|
||||||
|
root: {
|
||||||
|
topicsLearning: [{}],
|
||||||
|
topicsWantToLearn: [{}],
|
||||||
|
topicsLearned: [{}],
|
||||||
|
},
|
||||||
|
})
|
||||||
const username = ""
|
const username = ""
|
||||||
const { user } = useUser()
|
const { user } = useUser()
|
||||||
const avatarInputRef = React.useRef<HTMLInputElement>(null)
|
const avatarInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export const Route = createFileRoute("/_layout/_pages/_protected/tasks/")({
|
|||||||
|
|
||||||
function TaskComponent() {
|
function TaskComponent() {
|
||||||
const { filter } = Route.useSearch()
|
const { filter } = Route.useSearch()
|
||||||
const { me } = useAccount({ root: { tasks: [] } })
|
const { me } = useAccount({ root: { tasks: [{}] } })
|
||||||
const tasks = me?.root.tasks
|
const tasks = me?.root.tasks
|
||||||
const { deleteTask } = useTaskActions()
|
const { deleteTask } = useTaskActions()
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ export const TopicItem = React.forwardRef<HTMLAnchorElement, TopicItemProps>(
|
|||||||
)
|
)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { me } = useAccount({
|
const { me } = useAccount({
|
||||||
root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] },
|
root: {
|
||||||
|
topicsWantToLearn: [{}],
|
||||||
|
topicsLearning: [{}],
|
||||||
|
topicsLearned: [{}],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let p: {
|
let p: {
|
||||||
|
|||||||
@@ -5,23 +5,13 @@ import { atom } from "jotai"
|
|||||||
import { useMedia } from "@/hooks/use-media"
|
import { useMedia } from "@/hooks/use-media"
|
||||||
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 { LaAccount, ListOfTopics, Topic, UserRoot } from "@/lib/schema"
|
import { Topic } from "@/lib/schema"
|
||||||
import { LearningStateValue } from "@/lib/constants"
|
import { LearningStateValue } from "@/lib/constants"
|
||||||
import { useKeyDown } from "@/hooks/use-key-down"
|
import { useKeyDown } from "@/hooks/use-key-down"
|
||||||
import { TopicItem } from "./-item"
|
import { TopicItem } from "./-item"
|
||||||
|
|
||||||
interface TopicListProps {}
|
interface TopicListProps {}
|
||||||
|
|
||||||
interface MainTopicListProps extends TopicListProps {
|
|
||||||
me: {
|
|
||||||
root: {
|
|
||||||
topicsWantToLearn: ListOfTopics
|
|
||||||
topicsLearning: ListOfTopics
|
|
||||||
topicsLearned: ListOfTopics
|
|
||||||
} & UserRoot
|
|
||||||
} & LaAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PersonalTopic {
|
export interface PersonalTopic {
|
||||||
topic: Topic | null
|
topic: Topic | null
|
||||||
learningState: LearningStateValue
|
learningState: LearningStateValue
|
||||||
@@ -31,15 +21,13 @@ export const topicOpenPopoverForIdAtom = atom<string | null>(null)
|
|||||||
|
|
||||||
export const TopicList: React.FC<TopicListProps> = () => {
|
export const TopicList: React.FC<TopicListProps> = () => {
|
||||||
const { me } = useAccount({
|
const { me } = useAccount({
|
||||||
root: { topicsWantToLearn: [], topicsLearning: [], topicsLearned: [] },
|
root: {
|
||||||
|
topicsWantToLearn: [{}],
|
||||||
|
topicsLearning: [{}],
|
||||||
|
topicsLearned: [{}],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!me) return null
|
|
||||||
|
|
||||||
return <MainTopicList me={me} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
|
|
||||||
const isTablet = useMedia("(max-width: 640px)")
|
const isTablet = useMedia("(max-width: 640px)")
|
||||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(
|
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(
|
||||||
null,
|
null,
|
||||||
@@ -49,21 +37,24 @@ export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
|
|||||||
>(null)
|
>(null)
|
||||||
|
|
||||||
const personalTopics = React.useMemo(
|
const personalTopics = React.useMemo(
|
||||||
() => [
|
() =>
|
||||||
...me.root.topicsWantToLearn.map((topic) => ({
|
me
|
||||||
topic,
|
? [
|
||||||
learningState: "wantToLearn" as const,
|
...me.root.topicsWantToLearn.map((topic) => ({
|
||||||
})),
|
topic,
|
||||||
...me.root.topicsLearning.map((topic) => ({
|
learningState: "wantToLearn" as const,
|
||||||
topic,
|
})),
|
||||||
learningState: "learning" as const,
|
...me.root.topicsLearning.map((topic) => ({
|
||||||
})),
|
topic,
|
||||||
...me.root.topicsLearned.map((topic) => ({
|
learningState: "learning" as const,
|
||||||
topic,
|
})),
|
||||||
learningState: "learned" as const,
|
...me.root.topicsLearned.map((topic) => ({
|
||||||
})),
|
topic,
|
||||||
],
|
learningState: "learned" as const,
|
||||||
[me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned],
|
})),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
[me],
|
||||||
)
|
)
|
||||||
|
|
||||||
const next = () =>
|
const next = () =>
|
||||||
|
|||||||
@@ -85,9 +85,7 @@ const SearchComponent = () => {
|
|||||||
pages: PersonalPage[]
|
pages: PersonalPage[]
|
||||||
}>({ topics: [], links: [], pages: [] })
|
}>({ topics: [], links: [], pages: [] })
|
||||||
|
|
||||||
const { me } = useAccountOrGuest({
|
const { me } = useAccountOrGuest()
|
||||||
root: { personalLinks: [{}], personalPages: [{}] },
|
|
||||||
})
|
|
||||||
|
|
||||||
const globalGroup = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, {
|
const globalGroup = useCoState(PublicGlobalGroup, JAZZ_GLOBAL_GROUP_ID, {
|
||||||
root: {
|
root: {
|
||||||
@@ -112,14 +110,14 @@ const SearchComponent = () => {
|
|||||||
links:
|
links:
|
||||||
me?._type === "Anonymous"
|
me?._type === "Anonymous"
|
||||||
? []
|
? []
|
||||||
: me?.root.personalLinks?.filter(
|
: me?.root?.personalLinks?.filter(
|
||||||
(link: PersonalLink | null): link is PersonalLink =>
|
(link: PersonalLink | null): link is PersonalLink =>
|
||||||
link !== null && link.title.toLowerCase().startsWith(value),
|
link !== null && link.title.toLowerCase().startsWith(value),
|
||||||
) || [],
|
) || [],
|
||||||
pages:
|
pages:
|
||||||
me?._type === "Anonymous"
|
me?._type === "Anonymous"
|
||||||
? []
|
? []
|
||||||
: me?.root.personalPages.filter(
|
: me?.root?.personalPages?.filter(
|
||||||
(page): page is PersonalPage =>
|
(page): page is PersonalPage =>
|
||||||
page !== null &&
|
page !== null &&
|
||||||
page.title !== undefined &&
|
page.title !== undefined &&
|
||||||
|
|||||||
Reference in New Issue
Block a user