mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
feat: guest auth (#141)
* feat: Start using guest auth * feat: Implement more functionality to work as guest * chore: update package and tweak public route * chore: update root package json * chore: update web package json --------- Co-authored-by: Aslam H <iupin5212@gmail.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/cli": "^2.0.0-rc.12",
|
"@tauri-apps/cli": "^2.0.0-rc.12",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
||||||
"jazz-nodejs": "0.7.35-unique.2",
|
"jazz-nodejs": "0.7.35-guest-auth.5",
|
||||||
"react-icons": "^5.3.0"
|
"react-icons": "^5.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
import { SignedInClient } from "@/components/custom/clerk/signed-in-client"
|
"use client"
|
||||||
|
|
||||||
import { Sidebar } from "@/components/custom/sidebar/sidebar"
|
import { Sidebar } from "@/components/custom/sidebar/sidebar"
|
||||||
import { JazzClerkAuth, JazzProvider } from "@/lib/providers/jazz-provider"
|
|
||||||
import { currentUser } from "@clerk/nextjs/server"
|
|
||||||
import { CommandPalette } from "@/components/custom/command-palette/command-palette"
|
import { CommandPalette } from "@/components/custom/command-palette/command-palette"
|
||||||
|
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
import PublicHomeRoute from "@/components/routes/public/PublicHomeRoute"
|
||||||
|
|
||||||
export default async function PageLayout({ children }: { children: React.ReactNode }) {
|
export default function PageLayout({ children }: { children: React.ReactNode }) {
|
||||||
const user = await currentUser()
|
const { me } = useAccountOrGuest()
|
||||||
|
const pathname = usePathname()
|
||||||
|
|
||||||
if (!user) {
|
if (me._type === "Anonymous" && pathname === "/") {
|
||||||
return children
|
return <PublicHomeRoute />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JazzClerkAuth>
|
<div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
|
||||||
<SignedInClient>
|
<Sidebar />
|
||||||
<JazzProvider>
|
|
||||||
<div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
|
|
||||||
<Sidebar />
|
|
||||||
<CommandPalette />
|
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
{me._type !== "Anonymous" && <CommandPalette />}
|
||||||
<main className="relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
|
|
||||||
{children}
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
</main>
|
<main className="relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
|
||||||
</div>
|
{children}
|
||||||
</div>
|
</main>
|
||||||
</JazzProvider>
|
</div>
|
||||||
</SignedInClient>
|
</div>
|
||||||
</JazzClerkAuth>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Toaster } from "@/components/ui/sonner"
|
|||||||
import { ConfirmProvider } from "@/lib/providers/confirm-provider"
|
import { ConfirmProvider } from "@/lib/providers/confirm-provider"
|
||||||
import { DeepLinkProvider } from "@/lib/providers/deep-link-provider"
|
import { DeepLinkProvider } from "@/lib/providers/deep-link-provider"
|
||||||
import { GeistMono, GeistSans } from "./fonts"
|
import { GeistMono, GeistSans } from "./fonts"
|
||||||
|
import { JazzAndAuth } from "@/lib/providers/jazz-provider"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Learn Anything",
|
title: "Learn Anything",
|
||||||
@@ -27,7 +28,7 @@ export default function RootLayout({
|
|||||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<ConfirmProvider>
|
<ConfirmProvider>
|
||||||
{children}
|
<JazzAndAuth>{children}</JazzAndAuth>
|
||||||
<Toaster expand={false} />
|
<Toaster expand={false} />
|
||||||
</ConfirmProvider>
|
</ConfirmProvider>
|
||||||
</JotaiProvider>
|
</JotaiProvider>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { LinkSection } from "./partial/link-section"
|
|||||||
import { PageSection } from "./partial/page-section"
|
import { PageSection } from "./partial/page-section"
|
||||||
import { TopicSection } from "./partial/topic-section"
|
import { TopicSection } from "./partial/topic-section"
|
||||||
import { ProfileSection } from "./partial/profile-section"
|
import { ProfileSection } from "./partial/profile-section"
|
||||||
|
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||||
|
import { SignInButton } from "@clerk/nextjs"
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
@@ -109,7 +111,9 @@ const LogoAndSearch: React.FC = React.memo(() => {
|
|||||||
LogoAndSearch.displayName = "LogoAndSearch"
|
LogoAndSearch.displayName = "LogoAndSearch"
|
||||||
|
|
||||||
const SidebarContent: React.FC = React.memo(() => {
|
const SidebarContent: React.FC = React.memo(() => {
|
||||||
|
const { me } = useAccountOrGuest()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="bg-background relative flex h-full w-full shrink-0 flex-col">
|
<nav className="bg-background relative flex h-full w-full shrink-0 flex-col">
|
||||||
@@ -118,12 +122,19 @@ const SidebarContent: React.FC = React.memo(() => {
|
|||||||
</div>
|
</div>
|
||||||
<div tabIndex={-1} className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3">
|
<div tabIndex={-1} className="relative mb-0.5 mt-1.5 flex grow flex-col overflow-y-auto rounded-md px-3">
|
||||||
<div className="h-2 shrink-0" />
|
<div className="h-2 shrink-0" />
|
||||||
<LinkSection pathname={pathname} />
|
{me._type === "Account" && <LinkSection pathname={pathname} />}
|
||||||
<PageSection pathname={pathname} />
|
{me._type === "Account" && <PageSection pathname={pathname} />}
|
||||||
<TopicSection pathname={pathname} />
|
{me._type === "Account" && <TopicSection pathname={pathname} />}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<ProfileSection />
|
|
||||||
|
{me._type === "Account" ? (
|
||||||
|
<ProfileSection />
|
||||||
|
) : (
|
||||||
|
<div className="visible absolute inset-x-0 bottom-0 z-10 flex gap-8 p-2.5">
|
||||||
|
<SignInButton>Fake profile section</SignInButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface GraphNode {
|
|||||||
|
|
||||||
interface AutocompleteProps {
|
interface AutocompleteProps {
|
||||||
topics: GraphNode[]
|
topics: GraphNode[]
|
||||||
onSelect: (topic: GraphNode) => void
|
onSelect: (topic: string) => void
|
||||||
onInputChange: (value: string) => void
|
onInputChange: (value: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,18 +46,16 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl
|
|||||||
|
|
||||||
const handleSelect = useCallback(
|
const handleSelect = useCallback(
|
||||||
(topic: GraphNode) => {
|
(topic: GraphNode) => {
|
||||||
setInputValue(topic.prettyName)
|
// setInputValue(topicPrettyName)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
onSelect(topic)
|
onSelect(topic.name)
|
||||||
},
|
},
|
||||||
[onSelect]
|
[onSelect]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === "Enter" && filteredTopics.length > 0) {
|
if ((e.key === "Backspace" || e.key === "Delete") && inputRef.current?.value === "") {
|
||||||
handleSelect(filteredTopics[0])
|
|
||||||
} else if ((e.key === "Backspace" || e.key === "Delete") && inputRef.current?.value === "") {
|
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
setIsInitialOpen(true)
|
setIsInitialOpen(true)
|
||||||
}
|
}
|
||||||
@@ -65,7 +63,7 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl
|
|||||||
setHasInteracted(true)
|
setHasInteracted(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[filteredTopics, handleSelect, hasInteracted]
|
[hasInteracted]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleInputChange = useCallback(
|
const handleInputChange = useCallback(
|
||||||
@@ -143,6 +141,7 @@ export function Autocomplete({ topics = [], onSelect, onInputChange }: Autocompl
|
|||||||
{filteredTopics.map((topic, index) => (
|
{filteredTopics.map((topic, index) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={index}
|
key={index}
|
||||||
|
value={topic.name}
|
||||||
onSelect={() => handleSelect(topic)}
|
onSelect={() => handleSelect(topic)}
|
||||||
className="min-h-10 rounded-none px-3 py-1.5"
|
className="min-h-10 rounded-none px-3 py-1.5"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export function PublicHomeRoute() {
|
|||||||
const raw_graph_data = React.use(graph_data_promise) as GraphNode[]
|
const raw_graph_data = React.use(graph_data_promise) as GraphNode[]
|
||||||
const [filterQuery, setFilterQuery] = React.useState<string>("")
|
const [filterQuery, setFilterQuery] = React.useState<string>("")
|
||||||
|
|
||||||
const handleTopicSelect = (topicName: string) => {
|
const handleTopicSelect = (topic: string) => {
|
||||||
router.push(`/${topicName}`)
|
router.replace(`/${topic}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleInputChange = (value: string) => {
|
const handleInputChange = (value: string) => {
|
||||||
@@ -34,11 +34,7 @@ export function PublicHomeRoute() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-full w-screen">
|
<div className="relative h-full w-screen">
|
||||||
<ForceGraphClient
|
<ForceGraphClient raw_nodes={raw_graph_data} onNodeClick={handleTopicSelect} filter_query={filterQuery} />
|
||||||
raw_nodes={raw_graph_data}
|
|
||||||
onNodeClick={val => handleTopicSelect(val)}
|
|
||||||
filter_query={filterQuery}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="absolute left-1/2 top-1/2 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 transform max-sm:px-5">
|
<div className="absolute left-1/2 top-1/2 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 transform max-sm:px-5">
|
||||||
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
|
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }}>
|
||||||
@@ -53,11 +49,7 @@ export function PublicHomeRoute() {
|
|||||||
>
|
>
|
||||||
I want to learn
|
I want to learn
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<Autocomplete
|
<Autocomplete topics={raw_graph_data} onSelect={handleTopicSelect} onInputChange={handleInputChange} />
|
||||||
topics={raw_graph_data}
|
|
||||||
onSelect={topic => handleTopicSelect(topic.name)}
|
|
||||||
onInputChange={handleInputChange}
|
|
||||||
/>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import * as React from "react"
|
|||||||
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
||||||
import { ListOfTopics, Topic } from "@/lib/schema"
|
import { ListOfTopics, Topic } from "@/lib/schema"
|
||||||
import { LearningStateSelector } from "@/components/custom/learning-state-selector"
|
import { LearningStateSelector } from "@/components/custom/learning-state-selector"
|
||||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||||
import { LearningStateValue } from "@/lib/constants"
|
import { LearningStateValue } from "@/lib/constants"
|
||||||
|
import { toast } from "sonner"
|
||||||
|
|
||||||
interface TopicDetailHeaderProps {
|
interface TopicDetailHeaderProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }: TopicDetailHeaderProps) {
|
export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }: TopicDetailHeaderProps) {
|
||||||
const { me } = useAccount({
|
const { me } = useAccountOrGuest({
|
||||||
root: {
|
root: {
|
||||||
topicsWantToLearn: [],
|
topicsWantToLearn: [],
|
||||||
topicsLearning: [],
|
topicsLearning: [],
|
||||||
@@ -26,34 +27,44 @@ export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }
|
|||||||
learningState: LearningStateValue
|
learningState: LearningStateValue
|
||||||
} | null = null
|
} | null = null
|
||||||
|
|
||||||
const wantToLearnIndex = me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1
|
const wantToLearnIndex =
|
||||||
|
me?._type === "Anonymous" ? -1 : (me?.root.topicsWantToLearn.findIndex(t => t?.id === topic.id) ?? -1)
|
||||||
if (wantToLearnIndex !== -1) {
|
if (wantToLearnIndex !== -1) {
|
||||||
p = {
|
p = {
|
||||||
index: wantToLearnIndex,
|
index: wantToLearnIndex,
|
||||||
topic: me?.root.topicsWantToLearn[wantToLearnIndex],
|
// TODO: fix this type error by doing better conditionals on both index and p
|
||||||
|
topic: me && me._type !== "Anonymous" ? me.root.topicsWantToLearn[wantToLearnIndex] : undefined,
|
||||||
learningState: "wantToLearn"
|
learningState: "wantToLearn"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const learningIndex = me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1
|
const learningIndex =
|
||||||
|
me?._type === "Anonymous" ? -1 : (me?.root.topicsLearning.findIndex(t => t?.id === topic.id) ?? -1)
|
||||||
if (learningIndex !== -1) {
|
if (learningIndex !== -1) {
|
||||||
p = {
|
p = {
|
||||||
index: learningIndex,
|
index: learningIndex,
|
||||||
topic: me?.root.topicsLearning[learningIndex],
|
topic: me && me._type !== "Anonymous" ? me?.root.topicsLearning[learningIndex] : undefined,
|
||||||
learningState: "learning"
|
learningState: "learning"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const learnedIndex = me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1
|
const learnedIndex =
|
||||||
|
me?._type === "Anonymous" ? -1 : (me?.root.topicsLearned.findIndex(t => t?.id === topic.id) ?? -1)
|
||||||
if (learnedIndex !== -1) {
|
if (learnedIndex !== -1) {
|
||||||
p = {
|
p = {
|
||||||
index: learnedIndex,
|
index: learnedIndex,
|
||||||
topic: me?.root.topicsLearned[learnedIndex],
|
topic: me && me._type !== "Anonymous" ? me?.root.topicsLearned[learnedIndex] : undefined,
|
||||||
learningState: "learned"
|
learningState: "learned"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleAddToProfile = (learningState: LearningStateValue) => {
|
const handleAddToProfile = (learningState: LearningStateValue) => {
|
||||||
|
if (me?._type === "Anonymous") {
|
||||||
|
// TODO: handle better
|
||||||
|
toast.error("You need to sign in to add links to your personal list.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const topicLists: Record<LearningStateValue, (ListOfTopics | null) | undefined> = {
|
const topicLists: Record<LearningStateValue, (ListOfTopics | null) | undefined> = {
|
||||||
wantToLearn: me?.root.topicsWantToLearn,
|
wantToLearn: me?.root.topicsWantToLearn,
|
||||||
learning: me?.root.topicsLearning,
|
learning: me?.root.topicsLearning,
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import React, { useMemo, useRef } from "react"
|
|||||||
import { TopicDetailHeader } from "./Header"
|
import { TopicDetailHeader } from "./Header"
|
||||||
import { TopicSections } from "./partials/topic-sections"
|
import { TopicSections } from "./partials/topic-sections"
|
||||||
import { atom } from "jotai"
|
import { atom } from "jotai"
|
||||||
import { useAccount, useCoState } from "@/lib/providers/jazz-provider"
|
import { useAccount, useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||||
import { Topic } from "@/lib/schema"
|
import { useTopicData } from "@/hooks/use-topic-data"
|
||||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
|
||||||
|
|
||||||
interface TopicDetailRouteProps {
|
interface TopicDetailRouteProps {
|
||||||
topicName: string
|
topicName: string
|
||||||
@@ -15,10 +14,8 @@ interface TopicDetailRouteProps {
|
|||||||
export const openPopoverForIdAtom = atom<string | null>(null)
|
export const openPopoverForIdAtom = atom<string | null>(null)
|
||||||
|
|
||||||
export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
|
export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
|
||||||
const { me } = useAccount({ root: { personalLinks: [] } })
|
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
|
||||||
|
const { topic } = useTopicData(topicName, me)
|
||||||
const topicID = useMemo(() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me), [topicName, me])
|
|
||||||
const topic = useCoState(Topic, topicID, { latestGlobalGuide: { sections: [{ links: [] }] } })
|
|
||||||
// const { activeIndex, setActiveIndex, containerRef, linkRefs } = useLinkNavigation(allLinks)
|
// const { activeIndex, setActiveIndex, containerRef, linkRefs } = useLinkNavigation(allLinks)
|
||||||
const linksRefDummy = useRef<(HTMLLIElement | null)[]>([])
|
const linksRefDummy = useRef<(HTMLLIElement | null)[]>([])
|
||||||
const containerRefDummy = useRef<HTMLDivElement>(null)
|
const containerRefDummy = useRef<HTMLDivElement>(null)
|
||||||
@@ -37,8 +34,6 @@ export function TopicDetailRoute({ topicName }: TopicDetailRouteProps) {
|
|||||||
setActiveIndex={() => {}}
|
setActiveIndex={() => {}}
|
||||||
linkRefs={linksRefDummy}
|
linkRefs={linksRefDummy}
|
||||||
containerRef={containerRefDummy}
|
containerRef={containerRefDummy}
|
||||||
me={me}
|
|
||||||
personalLinks={me.root.personalLinks}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { cn, ensureUrlProtocol, generateUniqueSlug } from "@/lib/utils"
|
|||||||
import { LaAccount, Link as LinkSchema, PersonalLink, PersonalLinkLists, Topic, UserRoot } from "@/lib/schema"
|
import { LaAccount, Link as LinkSchema, PersonalLink, PersonalLinkLists, Topic, UserRoot } from "@/lib/schema"
|
||||||
import { openPopoverForIdAtom } from "../TopicDetailRoute"
|
import { openPopoverForIdAtom } from "../TopicDetailRoute"
|
||||||
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
import { LEARNING_STATES, LearningStateValue } from "@/lib/constants"
|
||||||
|
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||||
|
|
||||||
interface LinkItemProps {
|
interface LinkItemProps {
|
||||||
topic: Topic
|
topic: Topic
|
||||||
@@ -20,23 +21,24 @@ interface LinkItemProps {
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
index: number
|
index: number
|
||||||
setActiveIndex: (index: number) => void
|
setActiveIndex: (index: number) => void
|
||||||
me: {
|
|
||||||
root: {
|
|
||||||
personalLinks: PersonalLinkLists
|
|
||||||
} & UserRoot
|
|
||||||
} & LaAccount
|
|
||||||
personalLinks: PersonalLinkLists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkItem = React.memo(
|
export const LinkItem = React.memo(
|
||||||
React.forwardRef<HTMLLIElement, LinkItemProps>(
|
React.forwardRef<HTMLLIElement, LinkItemProps>(
|
||||||
({ topic, link, isActive, index, setActiveIndex, me, personalLinks }, ref) => {
|
({ topic, link, isActive, index, setActiveIndex }, ref) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const [, setOpenPopoverForId] = useAtom(openPopoverForIdAtom)
|
const [, setOpenPopoverForId] = useAtom(openPopoverForIdAtom)
|
||||||
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
|
||||||
|
|
||||||
|
const { me } = useAccountOrGuest({ root: { personalLinks: [] } });
|
||||||
|
|
||||||
|
const personalLinks = useMemo(() => {
|
||||||
|
if (!me || me._type === "Anonymous") return undefined;
|
||||||
|
return me?.root?.personalLinks || []
|
||||||
|
}, [me])
|
||||||
|
|
||||||
const personalLink = useMemo(() => {
|
const personalLink = useMemo(() => {
|
||||||
return personalLinks.find(pl => pl?.link?.id === link.id)
|
return personalLinks?.find(pl => pl?.link?.id === link.id)
|
||||||
}, [personalLinks, link.id])
|
}, [personalLinks, link.id])
|
||||||
|
|
||||||
const selectedLearningState = useMemo(() => {
|
const selectedLearningState = useMemo(() => {
|
||||||
@@ -53,6 +55,14 @@ export const LinkItem = React.memo(
|
|||||||
|
|
||||||
const handleSelectLearningState = useCallback(
|
const handleSelectLearningState = useCallback(
|
||||||
(learningState: LearningStateValue) => {
|
(learningState: LearningStateValue) => {
|
||||||
|
if (!personalLinks || !me || me?._type === "Anonymous") {
|
||||||
|
if (me?._type === "Anonymous") {
|
||||||
|
// TODO: handle better
|
||||||
|
toast.error("You need to sign in to add links to your personal list.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
};
|
||||||
|
|
||||||
const defaultToast = {
|
const defaultToast = {
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
position: "bottom-right" as const,
|
position: "bottom-right" as const,
|
||||||
|
|||||||
@@ -11,24 +11,9 @@ interface SectionProps {
|
|||||||
startIndex: number
|
startIndex: number
|
||||||
linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]>
|
linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]>
|
||||||
setActiveIndex: (index: number) => void
|
setActiveIndex: (index: number) => void
|
||||||
me: {
|
|
||||||
root: {
|
|
||||||
personalLinks: PersonalLinkLists
|
|
||||||
} & UserRoot
|
|
||||||
} & LaAccount
|
|
||||||
personalLinks: PersonalLinkLists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Section({
|
export function Section({ topic, section, activeIndex, setActiveIndex, startIndex, linkRefs }: SectionProps) {
|
||||||
topic,
|
|
||||||
section,
|
|
||||||
activeIndex,
|
|
||||||
setActiveIndex,
|
|
||||||
startIndex,
|
|
||||||
linkRefs,
|
|
||||||
me,
|
|
||||||
personalLinks
|
|
||||||
}: SectionProps) {
|
|
||||||
const [nLinksToLoad, setNLinksToLoad] = useState(10)
|
const [nLinksToLoad, setNLinksToLoad] = useState(10)
|
||||||
|
|
||||||
const linksToLoad = useMemo(() => {
|
const linksToLoad = useMemo(() => {
|
||||||
@@ -55,8 +40,6 @@ export function Section({
|
|||||||
ref={el => {
|
ref={el => {
|
||||||
linkRefs.current[startIndex + index] = el
|
linkRefs.current[startIndex + index] = el
|
||||||
}}
|
}}
|
||||||
me={me}
|
|
||||||
personalLinks={personalLinks}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton key={index} className="h-14 w-full xl:h-11" />
|
<Skeleton key={index} className="h-14 w-full xl:h-11" />
|
||||||
|
|||||||
@@ -9,12 +9,6 @@ interface TopicSectionsProps {
|
|||||||
setActiveIndex: (index: number) => void
|
setActiveIndex: (index: number) => void
|
||||||
linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]>
|
linkRefs: React.MutableRefObject<(HTMLLIElement | null)[]>
|
||||||
containerRef: React.RefObject<HTMLDivElement>
|
containerRef: React.RefObject<HTMLDivElement>
|
||||||
me: {
|
|
||||||
root: {
|
|
||||||
personalLinks: PersonalLinkLists
|
|
||||||
} & UserRoot
|
|
||||||
} & LaAccount
|
|
||||||
personalLinks: PersonalLinkLists
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopicSections({
|
export function TopicSections({
|
||||||
@@ -24,8 +18,6 @@ export function TopicSections({
|
|||||||
setActiveIndex,
|
setActiveIndex,
|
||||||
linkRefs,
|
linkRefs,
|
||||||
containerRef,
|
containerRef,
|
||||||
me,
|
|
||||||
personalLinks
|
|
||||||
}: TopicSectionsProps) {
|
}: TopicSectionsProps) {
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="flex w-full flex-1 flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
<div ref={containerRef} className="flex w-full flex-1 flex-col overflow-y-auto [scrollbar-gutter:stable]">
|
||||||
@@ -42,8 +34,6 @@ export function TopicSections({
|
|||||||
setActiveIndex={setActiveIndex}
|
setActiveIndex={setActiveIndex}
|
||||||
startIndex={sections.slice(0, sectionIndex).reduce((acc, s) => acc + (s?.links?.length || 0), 0)}
|
startIndex={sections.slice(0, sectionIndex).reduce((acc, s) => acc + (s?.links?.length || 0), 0)}
|
||||||
linkRefs={linkRefs}
|
linkRefs={linkRefs}
|
||||||
me={me}
|
|
||||||
personalLinks={personalLinks}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|||||||
15
web/hooks/use-topic-data.ts
Normal file
15
web/hooks/use-topic-data.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useMemo } from "react"
|
||||||
|
import { useCoState } from "@/lib/providers/jazz-provider"
|
||||||
|
import { PublicGlobalGroup } from "@/lib/schema/master/public-group"
|
||||||
|
import { Account, AnonymousJazzAgent, ID } from "jazz-tools"
|
||||||
|
import { Link, Topic } from "@/lib/schema"
|
||||||
|
|
||||||
|
const GLOBAL_GROUP_ID = process.env.NEXT_PUBLIC_JAZZ_GLOBAL_GROUP as ID<PublicGlobalGroup>
|
||||||
|
|
||||||
|
export function useTopicData(topicName: string, me: Account | AnonymousJazzAgent | undefined) {
|
||||||
|
const topicID = useMemo(() => me && Topic.findUnique({ topicName }, GLOBAL_GROUP_ID, me), [topicName, me])
|
||||||
|
|
||||||
|
const topic = useCoState(Topic, topicID, { latestGlobalGuide: { sections: [{ links: [] }] } })
|
||||||
|
|
||||||
|
return { topic }
|
||||||
|
}
|
||||||
@@ -3,104 +3,27 @@
|
|||||||
import { createJazzReactApp } from "jazz-react"
|
import { createJazzReactApp } from "jazz-react"
|
||||||
import { LaAccount } from "@/lib/schema"
|
import { LaAccount } from "@/lib/schema"
|
||||||
import { useClerk } from "@clerk/nextjs"
|
import { useClerk } from "@clerk/nextjs"
|
||||||
import { createContext, useMemo, useState } from "react"
|
import { useJazzClerkAuth } from "jazz-react-auth-clerk"
|
||||||
import { AuthMethodCtx } from "jazz-react"
|
|
||||||
|
|
||||||
const Jazz = createJazzReactApp({
|
const Jazz = createJazzReactApp({
|
||||||
AccountSchema: LaAccount
|
AccountSchema: LaAccount
|
||||||
})
|
})
|
||||||
|
|
||||||
export const { useAccount, useCoState, useAcceptInvite } = Jazz
|
export const { useAccount, useAccountOrGuest, useCoState, useAcceptInvite } = Jazz
|
||||||
|
|
||||||
export function JazzProvider({ children }: { children: React.ReactNode }) {
|
export function JazzAndAuth({ children }: { children: React.ReactNode }) {
|
||||||
return <Jazz.Provider peer="wss://mesh.jazz.tools/?key=example@gmail.com">{children}</Jazz.Provider>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const JazzClerkAuthCtx = createContext<{
|
|
||||||
errors: string[]
|
|
||||||
}>({
|
|
||||||
errors: []
|
|
||||||
})
|
|
||||||
|
|
||||||
export function JazzClerkAuth({ children }: { children: React.ReactNode }) {
|
|
||||||
const clerk = useClerk()
|
const clerk = useClerk()
|
||||||
const [errors, setErrors] = useState<string[]>([])
|
|
||||||
|
|
||||||
const authMethod = useMemo(() => {
|
const [auth, state] = useJazzClerkAuth(clerk)
|
||||||
return new BrowserClerkAuth(
|
|
||||||
{
|
|
||||||
onError: error => {
|
|
||||||
void clerk.signOut()
|
|
||||||
setErrors(errors => [...errors, error.toString()])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clerk
|
|
||||||
)
|
|
||||||
}, [clerk])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JazzClerkAuthCtx.Provider value={{ errors }}>
|
<>
|
||||||
<AuthMethodCtx.Provider value={authMethod}>{children}</AuthMethodCtx.Provider>
|
{state.errors.map((error) => (
|
||||||
</JazzClerkAuthCtx.Provider>
|
<div key={error}>{error}</div>
|
||||||
|
))}
|
||||||
|
<Jazz.Provider auth={auth || "guest"} peer="wss://mesh.jazz.tools/?key=example@gmail.com">
|
||||||
|
{children}
|
||||||
|
</Jazz.Provider>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
import { Account, AuthMethod, AuthResult, ID } from "jazz-tools"
|
|
||||||
import type { LoadedClerk } from "@clerk/types"
|
|
||||||
import { AgentSecret } from "cojson"
|
|
||||||
|
|
||||||
export class BrowserClerkAuth implements AuthMethod {
|
|
||||||
constructor(
|
|
||||||
public driver: BrowserClerkAuth.Driver,
|
|
||||||
private readonly clerkClient: LoadedClerk
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async start(): Promise<AuthResult> {
|
|
||||||
if (this.clerkClient.user) {
|
|
||||||
const storedCredentials = this.clerkClient.user.unsafeMetadata
|
|
||||||
if (storedCredentials.jazzAccountID) {
|
|
||||||
if (!storedCredentials.jazzAccountSecret) {
|
|
||||||
throw new Error("No secret for existing user")
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
type: "existing",
|
|
||||||
credentials: {
|
|
||||||
accountID: storedCredentials.jazzAccountID as ID<Account>,
|
|
||||||
secret: storedCredentials.jazzAccountSecret as AgentSecret
|
|
||||||
},
|
|
||||||
onSuccess: () => {},
|
|
||||||
onError: (error: string | Error) => {
|
|
||||||
this.driver.onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
type: "new",
|
|
||||||
creationProps: {
|
|
||||||
name: this.clerkClient.user.fullName || this.clerkClient.user.username || this.clerkClient.user.id
|
|
||||||
},
|
|
||||||
saveCredentials: async (credentials: { accountID: ID<Account>; secret: AgentSecret }) => {
|
|
||||||
await this.clerkClient.user?.update({
|
|
||||||
unsafeMetadata: {
|
|
||||||
jazzAccountID: credentials.accountID,
|
|
||||||
jazzAccountSecret: credentials.secret
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSuccess: () => {},
|
|
||||||
onError: (error: string | Error) => {
|
|
||||||
this.driver.onError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error("Not signed in")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace BrowserClerkAuth {
|
|
||||||
export interface Driver {
|
|
||||||
onError: (error: string | Error) => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"
|
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
|
||||||
|
|
||||||
const publicRoutes = ["/", "/sign-in(.*)", "/sign-up(.*)"]
|
const isPublicRoute = createRouteMatcher([
|
||||||
const isPublicRoute = createRouteMatcher(publicRoutes)
|
'/sign-in(.*)',
|
||||||
|
'/sign-up(.*)',
|
||||||
|
'/',
|
||||||
|
'/:topicName(.*)'
|
||||||
|
])
|
||||||
|
|
||||||
export default clerkMiddleware((auth, request) => {
|
export default clerkMiddleware((auth, request) => {
|
||||||
if (!isPublicRoute(request)) {
|
if (!isPublicRoute(request)) {
|
||||||
auth().protect()
|
auth().protect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
matcher: [
|
matcher: [
|
||||||
// Skip Next.js internals and all static files, unless found in search params
|
// Skip Next.js internals and all static files, unless found in search params
|
||||||
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
|
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
|
||||||
// Always run for API routes
|
// Always run for API routes
|
||||||
"/(api|trpc)(.*)"
|
'/(api|trpc)(.*)'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,9 +70,10 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.5.4",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"jazz-react": "0.7.35-unique.2",
|
"jazz-react": "0.7.35-guest-auth.5",
|
||||||
"jazz-react-auth-clerk": "0.7.33-new-auth.1",
|
"jazz-browser-auth-clerk": "0.7.35-guest-auth.5",
|
||||||
"jazz-tools": "0.7.35-unique.2",
|
"jazz-react-auth-clerk": "0.7.35-guest-auth.5",
|
||||||
|
"jazz-tools": "0.7.35-guest-auth.5",
|
||||||
"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