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:
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,
|
||||
description: "",
|
||||
status: "todo",
|
||||
createdAt: new Date()
|
||||
// updatedAt: new Date()
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
},
|
||||
{ owner: me._owner }
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import React, { useMemo, useState } from "react"
|
||||
import { TopicDetailHeader } from "./Header"
|
||||
import { TopicDetailHeader } from "./header"
|
||||
import { useAccountOrGuest, useCoState } from "@/lib/providers/jazz-provider"
|
||||
import { JAZZ_GLOBAL_GROUP_ID } from "@/lib/constants"
|
||||
import { Topic } from "@/lib/schema"
|
||||
|
||||
@@ -99,6 +99,9 @@ export const TopicDetailHeader = React.memo(function TopicDetailHeader({ topic }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-auto"></div>
|
||||
{/* <GuideCommunityToggle topicName={topic.name} /> */}
|
||||
|
||||
<LearningStateSelector
|
||||
showSearch={false}
|
||||
value={p?.learningState || ""}
|
||||
@@ -43,7 +43,6 @@ export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
|
||||
const isTablet = useMedia("(max-width: 640px)")
|
||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number | null>(null)
|
||||
const [keyboardActiveIndex, setKeyboardActiveIndex] = React.useState<number | null>(null)
|
||||
const router = useRouter()
|
||||
|
||||
const personalTopics = React.useMemo(
|
||||
() => [
|
||||
@@ -54,13 +53,6 @@ export const MainTopicList: React.FC<MainTopicListProps> = ({ me }) => {
|
||||
[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 prev = () => Math.max((activeItemIndex ?? 0) - 1, 0)
|
||||
|
||||
@@ -106,7 +106,7 @@ export const TopicItem = React.forwardRef<HTMLAnchorElement, TopicItemProps>(
|
||||
router.push(`/${topic.name}`)
|
||||
}
|
||||
},
|
||||
[router, topic.id]
|
||||
[router, topic.name]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user