mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
Q & A + journal (#174)
* tasks * task input * community route * added thread and list for community QA * answers thread * journal sidebar section * journal calendar * fix: stuff * fix: stuff * fix: stuff * chore: disable comunitty toggle * fix: typo import header --------- Co-authored-by: marshennikovaolga <marshennikova@gmail.com> Co-authored-by: Aslam H <iupin5212@gmail.com>
This commit is contained in:
67
package.json
67
package.json
@@ -1,36 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "learn-anything",
|
"name": "learn-anything",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun web",
|
"dev": "bun web",
|
||||||
"web": "cd web && bun dev",
|
"web": "cd web && bun dev",
|
||||||
"web:build": "bun run --filter '*' build",
|
"web:build": "bun run --filter '*' build",
|
||||||
"ts": "bun run --watch scripts/run.ts",
|
"ts": "bun run --watch scripts/run.ts",
|
||||||
"seed": "bun --watch scripts/seed.ts",
|
"seed": "bun --watch scripts/seed.ts",
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"app": "tauri dev",
|
"app": "tauri dev",
|
||||||
"app:build": "bun tauri build -b dmg -v"
|
"app:build": "bun tauri build -b dmg -v"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"web"
|
"web"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/themes": "^2.1.30",
|
"@tauri-apps/cli": "^2.0.0-rc.17",
|
||||||
"@tauri-apps/cli": "^2.0.0-rc.16",
|
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.0.0-rc.2",
|
"jazz-nodejs": "0.8.0"
|
||||||
"jazz-nodejs": "0.8.0"
|
},
|
||||||
},
|
"devDependencies": {
|
||||||
"devDependencies": {
|
"bun-types": "^1.1.29"
|
||||||
"bun-types": "^1.1.29"
|
},
|
||||||
},
|
"prettier": {
|
||||||
"prettier": {
|
"plugins": [
|
||||||
"plugins": [
|
"prettier-plugin-tailwindcss"
|
||||||
"prettier-plugin-tailwindcss"
|
],
|
||||||
],
|
"useTabs": true,
|
||||||
"useTabs": true,
|
"semi": false,
|
||||||
"semi": false,
|
"trailingComma": "none",
|
||||||
"trailingComma": "none",
|
"printWidth": 120,
|
||||||
"printWidth": 120,
|
"arrowParens": "avoid"
|
||||||
"arrowParens": "avoid"
|
},
|
||||||
},
|
"license": "MIT"
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
web/app/(pages)/community/[topicName]/page.tsx
Normal file
5
web/app/(pages)/community/[topicName]/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { CommunityTopicRoute } from "@/components/routes/community/CommunityTopicRoute"
|
||||||
|
|
||||||
|
export default function CommunityTopicPage({ params }: { params: { topicName: string } }) {
|
||||||
|
return <CommunityTopicRoute topicName={params.topicName} />
|
||||||
|
}
|
||||||
15
web/app/(pages)/journal/page.tsx
Normal file
15
web/app/(pages)/journal/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { JournalRoute } from "@/components/routes/journal/JournalRoute"
|
||||||
|
import { currentUser } from "@clerk/nextjs/server"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
import { get } from "ronin"
|
||||||
|
|
||||||
|
export default async function JournalPage() {
|
||||||
|
const user = await currentUser()
|
||||||
|
const flag = await get.featureFlag.with.name("JOURNAL")
|
||||||
|
|
||||||
|
if (!user?.emailAddresses.some(email => flag?.emails.includes(email.emailAddress))) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
return <JournalRoute />
|
||||||
|
}
|
||||||
@@ -74,3 +74,12 @@
|
|||||||
|
|
||||||
@import "./command-palette.css";
|
@import "./command-palette.css";
|
||||||
@import "./custom.css";
|
@import "./custom.css";
|
||||||
|
|
||||||
|
.scrollbar-hide {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollbar-hide::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
50
web/components/custom/GuideCommunityToggle.tsx
Normal file
50
web/components/custom/GuideCommunityToggle.tsx
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { useRouter, usePathname } from "next/navigation"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface GuideCommunityToggleProps {
|
||||||
|
topicName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GuideCommunityToggle: React.FC<GuideCommunityToggleProps> = ({ topicName }) => {
|
||||||
|
const router = useRouter()
|
||||||
|
const pathname = usePathname()
|
||||||
|
const [view, setView] = useState<"guide" | "community">("guide")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setView(pathname.includes("/community/") ? "community" : "guide")
|
||||||
|
}, [pathname])
|
||||||
|
|
||||||
|
const handleToggle = (newView: "guide" | "community") => {
|
||||||
|
setView(newView)
|
||||||
|
router.push(newView === "community" ? `/community/${topicName}` : `/${topicName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-accent/70 relative flex h-8 w-48 items-center rounded-md">
|
||||||
|
<div
|
||||||
|
className="absolute h-8 w-[calc(50%-4px)] rounded-md transition-all duration-300 ease-in-out"
|
||||||
|
style={{ left: view === "guide" ? "2px" : "calc(50% + 2px)" }}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 h-full flex-1 rounded-md text-sm font-medium transition-colors",
|
||||||
|
view === "guide" ? "text-primary bg-accent" : "text-primary/50"
|
||||||
|
)}
|
||||||
|
onClick={() => handleToggle("guide")}
|
||||||
|
>
|
||||||
|
Guide
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 h-full flex-1 rounded-md text-sm font-medium transition-colors",
|
||||||
|
view === "community" ? "text-primary bg-accent" : "text-primary/50"
|
||||||
|
)}
|
||||||
|
onClick={() => handleToggle("community")}
|
||||||
|
>
|
||||||
|
Community
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
65
web/components/custom/QuestionList.tsx
Normal file
65
web/components/custom/QuestionList.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { Input } from "../ui/input"
|
||||||
|
import { LaIcon } from "./la-icon"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
interface Question {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
author: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuestionListProps {
|
||||||
|
topicName: string
|
||||||
|
onSelectQuestion: (question: Question) => void
|
||||||
|
selectedQuestionId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuestionList({ topicName, onSelectQuestion, selectedQuestionId }: QuestionListProps) {
|
||||||
|
const [questions, setQuestions] = useState<Question[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mockQuestions: Question[] = Array(10)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, index) => ({
|
||||||
|
id: (index + 1).toString(),
|
||||||
|
title: "What can I do offline in Figma?",
|
||||||
|
author: "Ana",
|
||||||
|
timestamp: "13:35"
|
||||||
|
}))
|
||||||
|
setQuestions(mockQuestions)
|
||||||
|
}, [topicName])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col">
|
||||||
|
<div className="scrollbar-hide flex-grow overflow-y-auto">
|
||||||
|
{questions.map(question => (
|
||||||
|
<div
|
||||||
|
key={question.id}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-pointer flex-col gap-2 rounded p-4",
|
||||||
|
selectedQuestionId === question.id && "bg-red-500"
|
||||||
|
)}
|
||||||
|
onClick={() => onSelectQuestion(question)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row justify-between opacity-50">
|
||||||
|
<div className="flex flex-row items-center space-x-2">
|
||||||
|
<div className="h-6 w-6 rounded-full bg-slate-500" />
|
||||||
|
<p className="text-sm font-medium">{question.author}</p>
|
||||||
|
</div>
|
||||||
|
<p>{question.timestamp}</p>
|
||||||
|
</div>
|
||||||
|
<h3 className="font-medium">{question.title}</h3>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative mt-4">
|
||||||
|
<Input className="bg-input py-5 pr-10 focus:outline-none focus:ring-0" placeholder="Ask new question..." />
|
||||||
|
<button className="absolute right-2 top-1/2 -translate-y-1/2 transform opacity-60 hover:opacity-80">
|
||||||
|
<LaIcon name="Send" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
167
web/components/custom/QuestionThread.tsx
Normal file
167
web/components/custom/QuestionThread.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react"
|
||||||
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
|
import { LaIcon } from "./la-icon"
|
||||||
|
interface Answer {
|
||||||
|
id: string
|
||||||
|
author: string
|
||||||
|
content: string
|
||||||
|
timestamp: string
|
||||||
|
replies?: Answer[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QuestionThreadProps {
|
||||||
|
question: {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
author: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuestionThread({ question, onClose }: QuestionThreadProps) {
|
||||||
|
const [answers, setAnswers] = useState<Answer[]>([])
|
||||||
|
const [newAnswer, setNewAnswer] = useState("")
|
||||||
|
const [replyTo, setReplyTo] = useState<Answer | null>(null)
|
||||||
|
const [replyToAuthor, setReplyToAuthor] = useState<string | null>(null)
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const mockAnswers: Answer[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
author: "Noone",
|
||||||
|
content:
|
||||||
|
"Just press Command + Just press Command + Just press Command + Just press Command + Just press Command +",
|
||||||
|
timestamp: "14:40"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
setAnswers(mockAnswers)
|
||||||
|
}, [question.id])
|
||||||
|
|
||||||
|
const sendReply = (answer: Answer) => {
|
||||||
|
setReplyTo(answer)
|
||||||
|
setReplyToAuthor(answer.author)
|
||||||
|
setNewAnswer(`@${answer.author} `)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
const length = inputRef.current.value.length
|
||||||
|
inputRef.current.setSelectionRange(length, length)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newValue = e.target.value
|
||||||
|
setNewAnswer(newValue)
|
||||||
|
|
||||||
|
if (replyToAuthor && !newValue.startsWith(`@${replyToAuthor}`)) {
|
||||||
|
setReplyTo(null)
|
||||||
|
setReplyToAuthor(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendAnswer = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (newAnswer.trim()) {
|
||||||
|
const newReply: Answer = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
author: "Me",
|
||||||
|
content: newAnswer,
|
||||||
|
timestamp: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (replyTo) {
|
||||||
|
setAnswers(prevAnswers =>
|
||||||
|
prevAnswers.map(answer =>
|
||||||
|
answer.id === replyTo.id ? { ...answer, replies: [...(answer.replies || []), newReply] } : answer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
setAnswers(prevAnswers => [...prevAnswers, newReply])
|
||||||
|
}
|
||||||
|
setNewAnswer("")
|
||||||
|
setReplyTo(null)
|
||||||
|
setReplyToAuthor(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderAnswers = (answers: Answer[], isReply = false) => (
|
||||||
|
<div>
|
||||||
|
{answers.map(answer => (
|
||||||
|
<div key={answer.id} className={`flex-grow overflow-y-auto p-4 ${isReply ? "ml-3 border-l" : ""}`}>
|
||||||
|
<div className="flex items-center justify-between pb-1">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="bg-accent mr-2 h-6 w-6 rounded-full"></div>
|
||||||
|
<span className="text-sm">{answer.author}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button className="focus:outline-none">
|
||||||
|
<LaIcon name="Ellipsis" className="mr-2 size-4 shrink-0 opacity-30 hover:opacity-70" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<div className="w-[15px]">
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem onSelect={() => sendReply(answer)}>
|
||||||
|
<div className="mx-auto flex flex-row items-center gap-3">
|
||||||
|
<LaIcon name="Reply" className="size-4 shrink-0" />
|
||||||
|
Reply
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</div>
|
||||||
|
</DropdownMenu>
|
||||||
|
<span className="text-sm opacity-30">{answer.timestamp}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<p className="">{answer.content}</p>
|
||||||
|
<LaIcon name="ThumbsUp" className="ml-2 size-4 shrink-0 opacity-70" />
|
||||||
|
</div>
|
||||||
|
{answer.replies && renderAnswers(answer.replies, true)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-accent bg-background fixed bottom-0 right-0 top-0 z-50 flex h-full w-[40%] flex-col border-l">
|
||||||
|
<div className="border-accent flex w-full justify-between border-b p-4">
|
||||||
|
<div className="flex w-full flex-col">
|
||||||
|
<div className="mb-2 flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="bg-accent h-8 w-8 rounded-full"></div>
|
||||||
|
<h2 className="opacity-70">{question.author}</h2>
|
||||||
|
</div>
|
||||||
|
<button className="bg-accent rounded-full p-1.5 opacity-50 hover:opacity-80" onClick={onClose}>
|
||||||
|
<LaIcon name="X" className="text-primary" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-md mb-1 font-semibold">{question.title}</p>
|
||||||
|
<p className="text-sm opacity-70">{question.timestamp}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow overflow-y-auto">{renderAnswers(answers)}</div>
|
||||||
|
<div className="border-accent border-t p-4">
|
||||||
|
<form className="relative" onSubmit={sendAnswer}>
|
||||||
|
<div className="relative flex items-center">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={newAnswer}
|
||||||
|
onChange={changeInput}
|
||||||
|
placeholder="Answer the question..."
|
||||||
|
className="bg-input w-full rounded p-2 text-opacity-70 placeholder:text-opacity-50 focus:outline-none focus:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button className="absolute right-2 top-1/2 -translate-y-1/2 transform opacity-50 hover:opacity-90">
|
||||||
|
<LaIcon name="Send" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
113
web/components/custom/sidebar/partial/journal-section.tsx
Normal file
113
web/components/custom/sidebar/partial/journal-section.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import Link from "next/link"
|
||||||
|
import { usePathname } from "next/navigation"
|
||||||
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
|
import { LaIcon } from "../../la-icon"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useAuth, useUser } from "@clerk/nextjs"
|
||||||
|
import { getFeatureFlag } from "@/app/actions"
|
||||||
|
|
||||||
|
export const JournalSection: React.FC = () => {
|
||||||
|
const { me } = useAccount()
|
||||||
|
const journalEntries = me?.root?.journalEntries
|
||||||
|
const pathname = usePathname()
|
||||||
|
const isActive = pathname === "/journal"
|
||||||
|
|
||||||
|
const [isFetching, setIsFetching] = useState(false)
|
||||||
|
const [isFeatureActive, setIsFeatureActive] = useState(false)
|
||||||
|
const { isLoaded, isSignedIn } = useAuth()
|
||||||
|
const { user } = useUser()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function checkFeatureFlag() {
|
||||||
|
setIsFetching(true)
|
||||||
|
|
||||||
|
if (isLoaded && isSignedIn) {
|
||||||
|
const [data, err] = await getFeatureFlag({ name: "JOURNAL" })
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
console.error(err)
|
||||||
|
setIsFetching(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user?.emailAddresses.some(email => data.flag?.emails.includes(email.emailAddress))) {
|
||||||
|
setIsFeatureActive(true)
|
||||||
|
}
|
||||||
|
setIsFetching(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
checkFeatureFlag()
|
||||||
|
}, [isLoaded, isSignedIn, user])
|
||||||
|
|
||||||
|
if (!isLoaded || !isSignedIn) {
|
||||||
|
return <div className="py-2 text-center text-gray-500">Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!me) return null
|
||||||
|
|
||||||
|
if (!isFeatureActive) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group/journal flex flex-col gap-px py-2">
|
||||||
|
<JournalSectionHeader entriesCount={journalEntries?.length || 0} isActive={isActive} />
|
||||||
|
{journalEntries && journalEntries.length > 0 && <JournalEntryList entries={journalEntries} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JournalHeaderProps {
|
||||||
|
entriesCount: number
|
||||||
|
isActive: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const JournalSectionHeader: React.FC<JournalHeaderProps> = ({ entriesCount, isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[30px] items-center gap-px rounded-md",
|
||||||
|
isActive ? "bg-accent text-accent-foreground" : "hover:bg-accent hover:text-accent-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/journal"
|
||||||
|
className="flex flex-1 items-center justify-start rounded-md px-2 py-1 focus-visible:outline-none focus-visible:ring-0"
|
||||||
|
>
|
||||||
|
<p className="text-xs">
|
||||||
|
Journal
|
||||||
|
{entriesCount > 0 && <span className="text-muted-foreground ml-1">({entriesCount})</span>}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
interface JournalEntryListProps {
|
||||||
|
entries: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const JournalEntryList: React.FC<JournalEntryListProps> = ({ entries }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-px">
|
||||||
|
{entries.map((entry, index) => (
|
||||||
|
<JournalEntryItem key={index} entry={entry} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JournalEntryItemProps {
|
||||||
|
entry: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const JournalEntryItem: React.FC<JournalEntryItemProps> = ({ entry }) => (
|
||||||
|
<Link href={`/journal/${entry.id}`} className="group/journal-entry relative flex min-w-0 flex-1">
|
||||||
|
<div className="relative flex h-8 w-full items-center gap-2 rounded-md p-1.5 font-medium">
|
||||||
|
<div className="flex max-w-full flex-1 items-center gap-1.5 truncate text-sm">
|
||||||
|
<LaIcon name="FileText" className="opacity-60" />
|
||||||
|
<p className={cn("truncate opacity-95 group-hover/journal-entry:opacity-100")}>{entry.title}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
@@ -16,6 +16,7 @@ import { ProfileSection } from "./partial/profile-section"
|
|||||||
import { TaskSection } from "./partial/task-section"
|
import { TaskSection } from "./partial/task-section"
|
||||||
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
import { useAccountOrGuest } from "@/lib/providers/jazz-provider"
|
||||||
import { LaIcon } from "../la-icon"
|
import { LaIcon } from "../la-icon"
|
||||||
|
import { JournalSection } from "./partial/journal-section"
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean
|
||||||
@@ -123,6 +124,7 @@ const SidebarContent: React.FC = React.memo(() => {
|
|||||||
<div className="h-2 shrink-0" />
|
<div className="h-2 shrink-0" />
|
||||||
{me._type === "Account" && <LinkSection pathname={pathname} />}
|
{me._type === "Account" && <LinkSection pathname={pathname} />}
|
||||||
{me._type === "Account" && <TopicSection pathname={pathname} />}
|
{me._type === "Account" && <TopicSection pathname={pathname} />}
|
||||||
|
{me._type === "Account" && <JournalSection />}
|
||||||
{me._type === "Account" && <TaskSection pathname={pathname} />}
|
{me._type === "Account" && <TaskSection pathname={pathname} />}
|
||||||
{me._type === "Account" && <PageSection pathname={pathname} />}
|
{me._type === "Account" && <PageSection pathname={pathname} />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
74
web/components/routes/community/CommunityTopicRoute.tsx
Normal file
74
web/components/routes/community/CommunityTopicRoute.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { useAccountOrGuest, useCoState } from "@/lib/providers/jazz-provider"
|
||||||
|
import { ContentHeader, SidebarToggleButton } from "@/components/custom/content-header"
|
||||||
|
import { GuideCommunityToggle } from "@/components/custom/GuideCommunityToggle"
|
||||||
|
import { QuestionList } from "@/components/custom/QuestionList"
|
||||||
|
import { QuestionThread } from "@/components/custom/QuestionThread"
|
||||||
|
import { Topic } from "@/lib/schema"
|
||||||
|
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||||
|
|
||||||
|
interface CommunityTopicRouteProps {
|
||||||
|
topicName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Question {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
author: string
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommunityTopicRoute({ topicName }: CommunityTopicRouteProps) {
|
||||||
|
const { me } = useAccountOrGuest({ root: { personalLinks: [] } })
|
||||||
|
const topicID = useMemo(() => me && Topic.findUnique({ topicName }, JAZZ_GLOBAL_GROUP_ID, me), [topicName, me])
|
||||||
|
const topic = useCoState(Topic, topicID, { latestGlobalGuide: { sections: [] } })
|
||||||
|
|
||||||
|
const [selectedQuestion, setSelectedQuestion] = useState<Question | null>(null)
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-auto flex-col">
|
||||||
|
<ContentHeader className="px-6 py-4">
|
||||||
|
<div className="flex min-w-0 shrink-0 items-center gap-1.5">
|
||||||
|
<SidebarToggleButton />
|
||||||
|
<div className="flex min-h-0 flex-col items-start">
|
||||||
|
<p className="opacity-40">Topic</p>
|
||||||
|
<span className="truncate text-left font-bold lg:text-xl">{topic.prettyName}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow" />
|
||||||
|
<GuideCommunityToggle topicName={topic.name} />
|
||||||
|
</ContentHeader>
|
||||||
|
<div className="relative flex flex-1 justify-center overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`w-1/2 overflow-y-auto p-3 transition-all duration-300 ${
|
||||||
|
selectedQuestion ? "opacity-700 translate-x-[-50%]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<QuestionList
|
||||||
|
topicName={topic.name}
|
||||||
|
onSelectQuestion={(question: Question) => setSelectedQuestion(question)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selectedQuestion && (
|
||||||
|
<div className="absolute right-0 top-0 h-full w-1/2 overflow-y-auto">
|
||||||
|
<QuestionThread
|
||||||
|
question={{
|
||||||
|
id: selectedQuestion.id,
|
||||||
|
title: selectedQuestion.title,
|
||||||
|
author: selectedQuestion.author,
|
||||||
|
timestamp: selectedQuestion.timestamp
|
||||||
|
}}
|
||||||
|
onClose={() => setSelectedQuestion(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
114
web/components/routes/journal/JournalRoute.tsx
Normal file
114
web/components/routes/journal/JournalRoute.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
import { JournalEntry, JournalEntryLists } from "@/lib/schema/journal"
|
||||||
|
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
import { calendarFormatDate } from "@/lib/utils"
|
||||||
|
import { Calendar } from "@/components/ui/calendar"
|
||||||
|
|
||||||
|
export function JournalRoute() {
|
||||||
|
const [date, setDate] = useState<Date>(new Date())
|
||||||
|
const { me } = useAccount({ root: { journalEntries: [] } })
|
||||||
|
const [newNote, setNewNote] = useState<JournalEntry | null>(null)
|
||||||
|
|
||||||
|
const notes = me?.root?.journalEntries || (me ? JournalEntryLists.create([], { owner: me }) : [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("me:", me)
|
||||||
|
}, [me])
|
||||||
|
|
||||||
|
const selectDate = (selectedDate: Date | undefined) => {
|
||||||
|
if (selectedDate) {
|
||||||
|
setDate(selectedDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createNewNote = () => {
|
||||||
|
if (me) {
|
||||||
|
const newEntry = JournalEntry.create(
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
date: date,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
|
{ owner: me._owner }
|
||||||
|
)
|
||||||
|
setNewNote(newEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewNoteChange = (field: keyof JournalEntry, value: string) => {
|
||||||
|
if (newNote) {
|
||||||
|
setNewNote(prevNote => {
|
||||||
|
if (prevNote) {
|
||||||
|
return JournalEntry.create({ ...prevNote, [field]: value }, { owner: me!._owner })
|
||||||
|
}
|
||||||
|
return prevNote
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveNewNote = () => {
|
||||||
|
if (newNote && me?.root?.journalEntries) {
|
||||||
|
me.root.journalEntries.push(newNote)
|
||||||
|
setNewNote(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-auto flex-col">
|
||||||
|
<div className="relative flex flex-1 overflow-hidden">
|
||||||
|
<div className="flex-grow overflow-y-auto p-6">
|
||||||
|
{newNote ? (
|
||||||
|
<div className="mb-6 rounded-lg border p-4 shadow-sm">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Title"
|
||||||
|
value={newNote.title}
|
||||||
|
onChange={e => handleNewNoteChange("title", e.target.value)}
|
||||||
|
className="mb-2 w-full text-xl font-semibold"
|
||||||
|
/>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Content"
|
||||||
|
value={newNote.content as string}
|
||||||
|
onChange={e => handleNewNoteChange("content", e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<Button onClick={saveNewNote} className="mt-2">
|
||||||
|
Save Note
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{notes.map((entry, index) => (
|
||||||
|
<div key={index} className="mb-6 rounded-lg border p-4 shadow-sm">
|
||||||
|
<h2 className="mb-2 text-xl font-semibold">{entry?.title}</h2>
|
||||||
|
<div className="prose prose-sm max-w-none">
|
||||||
|
{entry?.content &&
|
||||||
|
(typeof entry.content === "string" ? (
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: entry.content }} />
|
||||||
|
) : (
|
||||||
|
<pre>{JSON.stringify(entry.content, null, 2)}</pre>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-sm opacity-70">{entry?.date && calendarFormatDate(new Date(entry.date))}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="w-[22%] border-l p-2">
|
||||||
|
<Calendar mode="single" selected={date} onSelect={selectDate} className="rounded-md border" />
|
||||||
|
<Button onClick={createNewNote} className="mt-4 w-full">
|
||||||
|
New Note
|
||||||
|
</Button>
|
||||||
|
<div className="p-2 text-sm opacity-50">
|
||||||
|
<p>Total notes: {notes.length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -30,8 +30,8 @@ export const TaskForm: React.FC<TaskFormProps> = ({}) => {
|
|||||||
title,
|
title,
|
||||||
description: "",
|
description: "",
|
||||||
status: "todo",
|
status: "todo",
|
||||||
createdAt: new Date()
|
createdAt: new Date(),
|
||||||
// updatedAt: new Date()
|
updatedAt: new Date()
|
||||||
},
|
},
|
||||||
{ owner: me._owner }
|
{ owner: me._owner }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import React, { useMemo, useState } from "react"
|
import React, { useMemo, useState } from "react"
|
||||||
import { TopicDetailHeader } from "./Header"
|
import { TopicDetailHeader } from "./header"
|
||||||
import { useAccountOrGuest, useCoState } from "@/lib/providers/jazz-provider"
|
import { useAccountOrGuest, useCoState } from "@/lib/providers/jazz-provider"
|
||||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||||
import { Topic } from "@/lib/schema"
|
import { Topic } from "@/lib/schema"
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-auto"></div>
|
||||||
|
{/* <GuideCommunityToggle topicName={topic.name} /> */}
|
||||||
|
|
||||||
<LearningStateSelector
|
<LearningStateSelector
|
||||||
showSearch={false}
|
showSearch={false}
|
||||||
value={p?.learningState || ""}
|
value={p?.learningState || ""}
|
||||||
@@ -43,7 +43,6 @@ 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>(null)
|
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(null)
|
||||||
const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
|
const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const personalTopics = React.useMemo(
|
const personalTopics = React.useMemo(
|
||||||
() => [
|
() => [
|
||||||
@@ -54,13 +53,6 @@ export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
|
|||||||
[me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned]
|
[me.root.topicsWantToLearn, me.root.topicsLearning, me.root.topicsLearned]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleEnter = React.useCallback(
|
|
||||||
(selectedTopic: Topic) => {
|
|
||||||
router.push(`/${selectedTopic.name}`)
|
|
||||||
},
|
|
||||||
[router]
|
|
||||||
)
|
|
||||||
|
|
||||||
const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalTopics?.length ?? 0) - 1)
|
const next = () => Math.min((activeItemIndex ?? 0) + 1, (personalTopics?.length ?? 0) - 1)
|
||||||
|
|
||||||
const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
|
const prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const TopicItem = React.forwardRef<HTMLAnchorElement, TopicItemProps>(
|
|||||||
router.push(`/${topic.name}`)
|
router.push(`/${topic.name}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[router, topic.id]
|
[router, topic.name]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { PersonalPageLists } from "./personal-page"
|
|||||||
import { PersonalLinkLists } from "./personal-link"
|
import { PersonalLinkLists } from "./personal-link"
|
||||||
import { ListOfTopics } from "./master/topic"
|
import { ListOfTopics } from "./master/topic"
|
||||||
import { ListOfTasks } from "./tasks"
|
import { ListOfTasks } from "./tasks"
|
||||||
|
import { JournalEntryLists } from "./journal"
|
||||||
|
|
||||||
declare module "jazz-tools" {
|
declare module "jazz-tools" {
|
||||||
interface Profile {
|
interface Profile {
|
||||||
@@ -35,6 +36,7 @@ export class UserRoot extends CoMap {
|
|||||||
topicsLearned = co.ref(ListOfTopics)
|
topicsLearned = co.ref(ListOfTopics)
|
||||||
|
|
||||||
tasks = co.ref(ListOfTasks)
|
tasks = co.ref(ListOfTasks)
|
||||||
|
journalEntries = co.ref(JournalEntryLists)
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LaAccount extends Account {
|
export class LaAccount extends Account {
|
||||||
@@ -64,7 +66,8 @@ export class LaAccount extends Account {
|
|||||||
topicsLearning: ListOfTopics.create([], { owner: this }),
|
topicsLearning: ListOfTopics.create([], { owner: this }),
|
||||||
topicsLearned: ListOfTopics.create([], { owner: this }),
|
topicsLearned: ListOfTopics.create([], { owner: this }),
|
||||||
|
|
||||||
tasks: ListOfTasks.create([], { owner: this })
|
tasks: ListOfTasks.create([], { owner: this }),
|
||||||
|
journalEntries: JournalEntryLists.create([], { owner: this })
|
||||||
},
|
},
|
||||||
{ owner: this }
|
{ owner: this }
|
||||||
)
|
)
|
||||||
|
|||||||
11
web/lib/schema/journal.ts
Normal file
11
web/lib/schema/journal.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { co, CoList, CoMap, Encoders } from "jazz-tools"
|
||||||
|
|
||||||
|
export class JournalEntry extends CoMap {
|
||||||
|
title = co.string
|
||||||
|
content = co.json()
|
||||||
|
date = co.encoded(Encoders.Date)
|
||||||
|
createdAt = co.encoded(Encoders.Date)
|
||||||
|
updatedAt = co.encoded(Encoders.Date)
|
||||||
|
}
|
||||||
|
|
||||||
|
export class JournalEntryLists extends CoList.Of(co.ref(JournalEntry)) {}
|
||||||
@@ -5,8 +5,7 @@ export class Task extends CoMap {
|
|||||||
description = co.optional.string
|
description = co.optional.string
|
||||||
status = co.literal("todo", "in_progress", "done")
|
status = co.literal("todo", "in_progress", "done")
|
||||||
createdAt = co.encoded(Encoders.Date)
|
createdAt = co.encoded(Encoders.Date)
|
||||||
// updatedAt = co.encoded(Encoders.Date)
|
updatedAt = co.encoded(Encoders.Date)
|
||||||
completedAt = co.optional.encoded(Encoders.Date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
export class ListOfTasks extends CoList.Of(co.ref(Task)) {}
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ export function isTextInput(element: Element): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calendarFormatDate(date: Date): string {
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export * from "./urls"
|
export * from "./urls"
|
||||||
export * from "./slug"
|
export * from "./slug"
|
||||||
export * from "./keyboard"
|
export * from "./keyboard"
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const ROUTE_PATTERNS = {
|
|||||||
"/settings(.*)",
|
"/settings(.*)",
|
||||||
"/tauri(.*)",
|
"/tauri(.*)",
|
||||||
"/onboarding(.*)",
|
"/onboarding(.*)",
|
||||||
"/tasks(.*)"
|
"/tasks(.*)",
|
||||||
|
"/journal(.*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"test": "jest"
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clerk/nextjs": "^5.6.0",
|
"@clerk/nextjs": "^5.6.3",
|
||||||
"@dnd-kit/core": "^6.1.0",
|
"@dnd-kit/core": "^6.1.0",
|
||||||
"@dnd-kit/modifiers": "^7.0.0",
|
"@dnd-kit/modifiers": "^7.0.0",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
@@ -37,46 +37,46 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
"@sentry/nextjs": "^8.31.0",
|
"@sentry/nextjs": "^8.32.0",
|
||||||
"@tanstack/react-virtual": "^3.10.8",
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
"@tiptap/core": "^2.7.2",
|
"@tiptap/core": "^2.7.4",
|
||||||
"@tiptap/extension-blockquote": "^2.7.2",
|
"@tiptap/extension-blockquote": "^2.7.4",
|
||||||
"@tiptap/extension-bold": "^2.7.2",
|
"@tiptap/extension-bold": "^2.7.4",
|
||||||
"@tiptap/extension-bullet-list": "^2.7.2",
|
"@tiptap/extension-bullet-list": "^2.7.4",
|
||||||
"@tiptap/extension-code": "^2.7.2",
|
"@tiptap/extension-code": "^2.7.4",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.7.2",
|
"@tiptap/extension-code-block-lowlight": "^2.7.4",
|
||||||
"@tiptap/extension-color": "^2.7.2",
|
"@tiptap/extension-color": "^2.7.4",
|
||||||
"@tiptap/extension-document": "^2.7.2",
|
"@tiptap/extension-document": "^2.7.4",
|
||||||
"@tiptap/extension-dropcursor": "^2.7.2",
|
"@tiptap/extension-dropcursor": "^2.7.4",
|
||||||
"@tiptap/extension-focus": "^2.7.2",
|
"@tiptap/extension-focus": "^2.7.4",
|
||||||
"@tiptap/extension-gapcursor": "^2.7.2",
|
"@tiptap/extension-gapcursor": "^2.7.4",
|
||||||
"@tiptap/extension-hard-break": "^2.7.2",
|
"@tiptap/extension-hard-break": "^2.7.4",
|
||||||
"@tiptap/extension-heading": "^2.7.2",
|
"@tiptap/extension-heading": "^2.7.4",
|
||||||
"@tiptap/extension-history": "^2.7.2",
|
"@tiptap/extension-history": "^2.7.4",
|
||||||
"@tiptap/extension-horizontal-rule": "^2.7.2",
|
"@tiptap/extension-horizontal-rule": "^2.7.4",
|
||||||
"@tiptap/extension-image": "^2.7.2",
|
"@tiptap/extension-image": "^2.7.4",
|
||||||
"@tiptap/extension-italic": "^2.7.2",
|
"@tiptap/extension-italic": "^2.7.4",
|
||||||
"@tiptap/extension-link": "^2.7.2",
|
"@tiptap/extension-link": "^2.7.4",
|
||||||
"@tiptap/extension-list-item": "^2.7.2",
|
"@tiptap/extension-list-item": "^2.7.4",
|
||||||
"@tiptap/extension-ordered-list": "^2.7.2",
|
"@tiptap/extension-ordered-list": "^2.7.4",
|
||||||
"@tiptap/extension-paragraph": "^2.7.2",
|
"@tiptap/extension-paragraph": "^2.7.4",
|
||||||
"@tiptap/extension-placeholder": "^2.7.2",
|
"@tiptap/extension-placeholder": "^2.7.4",
|
||||||
"@tiptap/extension-strike": "^2.7.2",
|
"@tiptap/extension-strike": "^2.7.4",
|
||||||
"@tiptap/extension-task-item": "^2.7.2",
|
"@tiptap/extension-task-item": "^2.7.4",
|
||||||
"@tiptap/extension-task-list": "^2.7.2",
|
"@tiptap/extension-task-list": "^2.7.4",
|
||||||
"@tiptap/extension-text": "^2.7.2",
|
"@tiptap/extension-text": "^2.7.4",
|
||||||
"@tiptap/extension-typography": "^2.7.2",
|
"@tiptap/extension-typography": "^2.7.4",
|
||||||
"@tiptap/pm": "^2.7.2",
|
"@tiptap/pm": "^2.7.4",
|
||||||
"@tiptap/react": "^2.7.2",
|
"@tiptap/react": "^2.7.4",
|
||||||
"@tiptap/starter-kit": "^2.7.2",
|
"@tiptap/starter-kit": "^2.7.4",
|
||||||
"@tiptap/suggestion": "^2.7.2",
|
"@tiptap/suggestion": "^2.7.4",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"cheerio": "1.0.0",
|
"cheerio": "1.0.0",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.5.6",
|
"framer-motion": "^11.9.0",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"jazz-browser-auth-clerk": "0.8.0",
|
"jazz-browser-auth-clerk": "0.8.0",
|
||||||
"jazz-react": "0.8.0",
|
"jazz-react": "0.8.0",
|
||||||
@@ -86,8 +86,9 @@
|
|||||||
"lowlight": "^3.1.0",
|
"lowlight": "^3.1.0",
|
||||||
"lucide-react": "^0.429.0",
|
"lucide-react": "^0.429.0",
|
||||||
"next": "14.2.10",
|
"next": "14.2.10",
|
||||||
|
"@clerk/themes": "^2.1.33",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"nuqs": "^1.19.2",
|
"nuqs": "^1.19.3",
|
||||||
"query-string": "^9.1.0",
|
"query-string": "^9.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
@@ -100,7 +101,7 @@
|
|||||||
"streaming-markdown": "^0.0.14",
|
"streaming-markdown": "^0.0.14",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vaul": "^0.9.4",
|
"vaul": "^0.9.9",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
"zsa": "^0.6.0",
|
"zsa": "^0.6.0",
|
||||||
"zsa-react": "^0.2.3"
|
"zsa-react": "^0.2.3"
|
||||||
@@ -110,8 +111,8 @@
|
|||||||
"@testing-library/jest-dom": "^6.5.0",
|
"@testing-library/jest-dom": "^6.5.0",
|
||||||
"@testing-library/react": "^16.0.1",
|
"@testing-library/react": "^16.0.1",
|
||||||
"@types/jest": "^29.5.13",
|
"@types/jest": "^29.5.13",
|
||||||
"@types/node": "^22.6.1",
|
"@types/node": "^22.7.4",
|
||||||
"@types/react": "^18.3.8",
|
"@types/react": "^18.3.10",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
@@ -119,7 +120,7 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"postcss": "^8.4.47",
|
"postcss": "^8.4.47",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
"prettier-plugin-tailwindcss": "^0.6.8",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user