mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-26 10:18:34 +02:00
perf: Lazy loading for links in topic sections (#127)
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
"web"
|
"web"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jazz-nodejs": "^0.7.34",
|
"jazz-nodejs": "0.7.35-unique.2",
|
||||||
"react-icons": "^5.3.0"
|
"react-icons": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React from "react"
|
import React, { useRef } from "react"
|
||||||
import { TopicDetailHeader } from "./Header"
|
import { TopicDetailHeader } from "./Header"
|
||||||
import { TopicSections } from "./partials/topic-sections"
|
import { TopicSections } from "./partials/topic-sections"
|
||||||
import { useLinkNavigation } from "./use-link-navigation"
|
import { useLinkNavigation } from "./use-link-navigation"
|
||||||
@@ -16,8 +16,10 @@ export const openPopoverForIdAtom = atom<string | null>(null)
|
|||||||
|
|
||||||
export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
|
export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
|
||||||
const { me } = useAccount({ root: { personalLinks: [] } })
|
const { me } = useAccount({ root: { personalLinks: [] } })
|
||||||
const { topic, allLinks } = useTopicData(topicName)
|
const { topic } = useTopicData(topicName, me)
|
||||||
const { activeIndex, setActiveIndex, containerRef, linkRefs } = useLinkNavigation(allLinks)
|
// const { activeIndex, setActiveIndex, containerRef, linkRefs } = useLinkNavigation(allLinks)
|
||||||
|
const linksRefDummy = useRef<(HTMLLIElement | null)[]>([])
|
||||||
|
const containerRefDummy = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
if (!topic || !me) {
|
if (!topic || !me) {
|
||||||
return null
|
return null
|
||||||
@@ -29,10 +31,10 @@ export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
|
|||||||
<TopicSections
|
<TopicSections
|
||||||
topic={topic}
|
topic={topic}
|
||||||
sections={topic.latestGlobalGuide?.sections}
|
sections={topic.latestGlobalGuide?.sections}
|
||||||
activeIndex={activeIndex}
|
activeIndex={0}
|
||||||
setActiveIndex={setActiveIndex}
|
setActiveIndex={() => {}}
|
||||||
linkRefs={linkRefs}
|
linkRefs={linksRefDummy}
|
||||||
containerRef={containerRef}
|
containerRef={containerRefDummy}
|
||||||
me={me}
|
me={me}
|
||||||
personalLinks={me.root.personalLinks}
|
personalLinks={me.root.personalLinks}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import React from "react"
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { LinkItem } from "./link-item"
|
import { LinkItem } from "./link-item"
|
||||||
import { LaAccount, PersonalLinkLists, Section as SectionSchema, Topic, UserRoot } from "@/lib/schema"
|
import { LaAccount, PersonalLinkLists, Section as SectionSchema, Topic, UserRoot } from "@/lib/schema"
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton"
|
||||||
|
import { Loader2 } from "lucide-react"
|
||||||
|
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
@@ -27,6 +29,12 @@ export function Section({
|
|||||||
me,
|
me,
|
||||||
personalLinks
|
personalLinks
|
||||||
}: SectionProps) {
|
}: SectionProps) {
|
||||||
|
const [nLinksToLoad, setNLinksToLoad] = useState(10);
|
||||||
|
|
||||||
|
const linksToLoad = useMemo(() => {
|
||||||
|
return section.links?.slice(0, nLinksToLoad)
|
||||||
|
}, [section.links, nLinksToLoad])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-4 px-6 py-2 max-lg:px-4">
|
<div className="flex items-center gap-4 px-6 py-2 max-lg:px-4">
|
||||||
@@ -35,9 +43,9 @@ export function Section({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-px py-2">
|
<div className="flex flex-col gap-px py-2">
|
||||||
{section.links?.map(
|
{linksToLoad?.map(
|
||||||
(link, index) =>
|
(link, index) =>
|
||||||
link?.url && (
|
link?.url ? (
|
||||||
<LinkItem
|
<LinkItem
|
||||||
key={index}
|
key={index}
|
||||||
topic={topic}
|
topic={topic}
|
||||||
@@ -51,9 +59,52 @@ export function Section({
|
|||||||
me={me}
|
me={me}
|
||||||
personalLinks={personalLinks}
|
personalLinks={personalLinks}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Skeleton key={index} className="h-14 xl:h-11 w-full" />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
{section.links?.length && section.links?.length > nLinksToLoad && (
|
||||||
|
<LoadMoreSpinner onLoadMore={() => setNLinksToLoad(n => n + 10)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LoadMoreSpinner = ({ onLoadMore }: { onLoadMore: () => void }) => {
|
||||||
|
const spinnerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const handleIntersection = useCallback(
|
||||||
|
(entries: IntersectionObserverEntry[]) => {
|
||||||
|
const [entry] = entries
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
onLoadMore()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onLoadMore]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(handleIntersection, {
|
||||||
|
root: null,
|
||||||
|
rootMargin: "0px",
|
||||||
|
threshold: 1.0,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (spinnerRef.current) {
|
||||||
|
observer.observe(spinnerRef.current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (spinnerRef.current) {
|
||||||
|
observer.unobserve(spinnerRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [handleIntersection])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={spinnerRef} className="flex justify-center py-4">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,29 +1,15 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useCoState } from "@/lib/providers/jazz-provider"
|
import { useCoState } from "@/lib/providers/jazz-provider"
|
||||||
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
||||||
import { ID } from "jazz-tools"
|
import { Account, ID } from "jazz-tools"
|
||||||
import { Link } from "@/lib/schema"
|
import { Link, Topic } from "@/lib/schema"
|
||||||
|
|
||||||
const GLOBAL_GROUP_ID = process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>
|
const GLOBAL_GROUP_ID = process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>
|
||||||
|
|
||||||
export function useTopicData(topicName: string) {
|
export function useTopicData(topicName: string, me: Account | undefined) {
|
||||||
const group = useCoState(PublicGlobalGroup, GLOBAL_GROUP_ID, {
|
const topicID = useMemo(() => me && Topic.findUnique({topicName}, GLOBAL_GROUP_ID, me), [topicName, me])
|
||||||
root: { topics: [] }
|
|
||||||
})
|
|
||||||
|
|
||||||
// const topic = useCoState(Topic, "co_zS3TH4Lkj5MK9GEehinxhjjNTxB" as ID<Topic>, {})
|
const topic = useCoState(Topic, topicID, {latestGlobalGuide: {sections: [{links: []}]}})
|
||||||
const topic = useMemo(
|
|
||||||
() => group?.root.topics.find(topic => topic?.name === topicName),
|
|
||||||
[group?.root.topics, topicName]
|
|
||||||
)
|
|
||||||
|
|
||||||
const allLinks = useMemo(() => {
|
return { topic }
|
||||||
if (!topic?.latestGlobalGuide?.sections) return []
|
|
||||||
|
|
||||||
return topic.latestGlobalGuide.sections.flatMap(
|
|
||||||
section => section?.links?.filter((link): link is Link => !!link?.url) ?? []
|
|
||||||
)
|
|
||||||
}, [topic?.latestGlobalGuide?.sections])
|
|
||||||
|
|
||||||
return { topic, allLinks }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export class LaAccount extends Account {
|
|||||||
// so just do default profile create provided by jazz-tools
|
// so just do default profile create provided by jazz-tools
|
||||||
super.migrate(creationProps)
|
super.migrate(creationProps)
|
||||||
|
|
||||||
|
console.log("In migration", this._refs.root, creationProps)
|
||||||
|
|
||||||
if (!this._refs.root && creationProps) {
|
if (!this._refs.root && creationProps) {
|
||||||
this.root = UserRoot.create(
|
this.root = UserRoot.create(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -67,9 +67,9 @@
|
|||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.3.31",
|
"framer-motion": "^11.3.31",
|
||||||
"jazz-react": "0.7.35-new-auth.1",
|
"jazz-react": "0.7.35-unique.2",
|
||||||
"jazz-react-auth-clerk": "0.7.33-new-auth.1",
|
"jazz-react-auth-clerk": "0.7.33-new-auth.1",
|
||||||
"jazz-tools": "0.7.35-new-auth.0",
|
"jazz-tools": "0.7.35-unique.2",
|
||||||
"jotai": "^2.9.3",
|
"jotai": "^2.9.3",
|
||||||
"lowlight": "^3.1.0",
|
"lowlight": "^3.1.0",
|
||||||
"lucide-react": "^0.429.0",
|
"lucide-react": "^0.429.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user