diff --git a/apps/desktop2/src/components/main/body/calendars.tsx b/apps/desktop2/src/components/main/body/calendars.tsx new file mode 100644 index 0000000000..6f74349158 --- /dev/null +++ b/apps/desktop2/src/components/main/body/calendars.tsx @@ -0,0 +1,144 @@ +import { clsx } from "clsx"; +import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameMonth, startOfMonth } from "date-fns"; +import { CalendarIcon, FileTextIcon } from "lucide-react"; + +import { CalendarStructure } from "@hypr/ui/components/block/calendar-structure"; +import * as persisted from "../../../store/tinybase/persisted"; +import { type Tab, useTabs } from "../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "./shared"; + +export const TabItemCalendar: TabItem = ({ tab, handleClose, handleSelect }) => { + return ( + } + title={"Calendar"} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +export function TabContentCalendar({ tab }: { tab: Tab }) { + if (tab.type !== "calendars") { + return null; + } + + const { openCurrent } = useTabs(); + const monthStart = startOfMonth(tab.month); + const monthEnd = endOfMonth(tab.month); + const days = eachDayOfInterval({ start: monthStart, end: monthEnd }).map((day) => format(day, "yyyy-MM-dd")); + const startDayOfWeek = getDay(monthStart); + const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + + const handlePreviousMonth = () => { + openCurrent({ ...tab, month: addMonths(tab.month, -1) }); + }; + + const handleNextMonth = () => { + openCurrent({ ...tab, month: addMonths(tab.month, 1) }); + }; + + const handleToday = () => { + openCurrent({ ...tab, month: new Date() }); + }; + + return ( + + {days.map((day) => ( + + ))} + + ); +} + +function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrentMonth: boolean }) { + const eventIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.eventsByDate, + day, + persisted.STORE_ID, + ); + + const sessionIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.sessionByDateWithoutEvent, + day, + persisted.STORE_ID, + ); + + const dayNumber = format(new Date(day), "d"); + const isToday = format(new Date(), "yyyy-MM-dd") === day; + const dayOfWeek = getDay(new Date(day)); + const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; + + return ( +
+
+
+
+ + {dayNumber} + +
+
+
+ +
+
+ {eventIds.map((eventId) => )} + {sessionIds.map((sessionId) => )} +
+
+
+ ); +} + +function TabContentCalendarDayEvents({ eventId }: { eventId: string }) { + const event = persisted.UI.useRow("events", eventId, persisted.STORE_ID); + + return ( +
+ +
+ {event.title} +
+
+ ); +} + +function TabContentCalendarDaySessions({ sessionId }: { sessionId: string }) { + const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); + return ( +
+ +
+ {session.title} +
+
+ ); +} diff --git a/apps/desktop2/src/components/main/body/events.tsx b/apps/desktop2/src/components/main/body/events.tsx new file mode 100644 index 0000000000..e4178985c2 --- /dev/null +++ b/apps/desktop2/src/components/main/body/events.tsx @@ -0,0 +1,26 @@ +import { CalendarIcon } from "lucide-react"; + +import * as persisted from "../../../store/tinybase/persisted"; +import { rowIdfromTab, type Tab } from "../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "./shared"; + +export const TabItemEvent: TabItem = ({ tab, handleClose, handleSelect }) => { + const title = persisted.UI.useCell("events", rowIdfromTab(tab), "title", persisted.STORE_ID); + + return ( + } + title={title ?? ""} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +export function TabContentEvent({ tab }: { tab: Tab }) { + const id = rowIdfromTab(tab); + const event = persisted.UI.useRow("events", id, persisted.STORE_ID); + + return
{JSON.stringify(event, null, 2)}
; +} diff --git a/apps/desktop2/src/components/main/body/folders.tsx b/apps/desktop2/src/components/main/body/folders.tsx new file mode 100644 index 0000000000..4e14478a2f --- /dev/null +++ b/apps/desktop2/src/components/main/body/folders.tsx @@ -0,0 +1,215 @@ +import { FolderIcon, StickyNoteIcon } from "lucide-react"; + +import * as persisted from "../../../store/tinybase/persisted"; +import { type Tab } from "../../../store/zustand/tabs"; +import { useTabs } from "../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "./shared"; + +export const TabItemFolder: TabItem = ({ tab, handleClose, handleSelect }) => { + if (tab.type === "folders" && tab.id === null) { + return ; + } + + if (tab.type === "folders" && tab.id !== null) { + return ; + } + + return null; +}; + +const TabItemFolderAll: TabItem = ({ tab, handleClose, handleSelect }) => { + return ( + } + title={"Folder"} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +const TabItemFolderSpecific: TabItem = ({ tab, handleClose, handleSelect }) => { + if (tab.type !== "folders" || tab.id === null) { + return null; + } + + const folderName = persisted.UI.useCell("folders", tab.id, "name", persisted.STORE_ID); + + return ( + } + title={folderName ?? ""} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +export function TabContentFolder({ tab }: { tab: Tab }) { + if (tab.type !== "folders") { + return null; + } + + // If tab.id is null, show top-level folders + if (tab.id === null) { + return ; + } + + // If tab.id is a folder, show that folder's contents + return ; +} + +function TabContentFolderTopLevel() { + const topLevelFolderIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.foldersByParent, + "", + persisted.STORE_ID, + ); + + return ( +
+

All Folders

+
+ {topLevelFolderIds?.map((folderId) => )} +
+
+ ); +} + +function FolderCard({ folderId }: { folderId: string }) { + const folder = persisted.UI.useRow("folders", folderId, persisted.STORE_ID); + const { openCurrent } = useTabs(); + + // Count children + const childFolderIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.foldersByParent, + folderId, + persisted.STORE_ID, + ); + + const sessionIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.sessionsByFolder, + folderId, + persisted.STORE_ID, + ); + + const childCount = (childFolderIds?.length ?? 0) + (sessionIds?.length ?? 0); + + return ( +
openCurrent({ type: "folders", id: folderId, active: true })} + > + + {folder.name} + {childCount > 0 && {childCount} items} +
+ ); +} + +function TabContentFolderSpecific({ folderId }: { folderId: string }) { + const childFolderIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.foldersByParent, + folderId, + persisted.STORE_ID, + ); + + const sessionIds = persisted.UI.useSliceRowIds( + persisted.INDEXES.sessionsByFolder, + folderId, + persisted.STORE_ID, + ); + + return ( +
+ + + {(childFolderIds?.length ?? 0) > 0 && ( +
+

Folders

+
+ {childFolderIds!.map((childId) => )} +
+
+ )} + + {(sessionIds?.length ?? 0) > 0 && ( +
+

Notes

+
+ {sessionIds!.map((sessionId) => )} +
+
+ )} + + {(childFolderIds?.length ?? 0) === 0 && (sessionIds?.length ?? 0) === 0 && ( +
+ This folder is empty +
+ )} +
+ ); +} + +function TabContentFolderBreadcrumb({ folderId }: { folderId: string }) { + const { openCurrent } = useTabs(); + + const folderIds = persisted.UI.useLinkedRowIds( + "folderToParentFolder", + folderId, + persisted.STORE_ID, + ); + + if (!folderIds || folderIds.length === 0) { + return null; + } + + const folderChain = [...folderIds].reverse(); + + return ( +
+ + {folderChain.map((id) => { + const isLast = id === folderId; + return ( +
+ / + +
+ ); + })} +
+ ); +} + +function TabContentFolderBreadcrumbItem({ folderId }: { folderId: string }) { + const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID); + return {folderName}; +} + +function FolderSessionItem({ sessionId }: { sessionId: string }) { + const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); + const { openCurrent } = useTabs(); + + return ( +
openCurrent({ type: "sessions", id: sessionId, active: true })} + > + + {session.title} +
+ ); +} diff --git a/apps/desktop2/src/components/main/body/humans.tsx b/apps/desktop2/src/components/main/body/humans.tsx new file mode 100644 index 0000000000..7979af2c31 --- /dev/null +++ b/apps/desktop2/src/components/main/body/humans.tsx @@ -0,0 +1,31 @@ +import { UserIcon } from "lucide-react"; + +import * as persisted from "../../../store/tinybase/persisted"; +import { type Tab } from "../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "./shared"; + +export const TabItemHuman: TabItem = ({ tab, handleClose, handleSelect }) => { + if (tab.type !== "humans") { + throw new Error("non_human_tab"); + } + + const title = persisted.UI.useCell("humans", tab.id, "name", persisted.STORE_ID); + + return ( + } + title={title ?? "Human"} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +export function TabContentHuman({ tab }: { tab: Tab }) { + if (tab.type !== "humans") { + throw new Error("non_human_tab"); + } + + return
Human
; +} diff --git a/apps/desktop2/src/components/main/body/index.tsx b/apps/desktop2/src/components/main/body/index.tsx new file mode 100644 index 0000000000..f11d0a8567 --- /dev/null +++ b/apps/desktop2/src/components/main/body/index.tsx @@ -0,0 +1,109 @@ +import clsx from "clsx"; +import { Reorder } from "motion/react"; + +import { type Tab, uniqueIdfromTab, useTabs } from "../../../store/zustand/tabs"; + +import { TabContentCalendar, TabItemCalendar } from "./calendars"; +import { TabContentEvent, TabItemEvent } from "./events"; +import { TabContentFolder, TabItemFolder } from "./folders"; +import { TabContentHuman, TabItemHuman } from "./humans"; +import { TabContentNote, TabItemNote } from "./sessions"; + +export function MainContent() { + const { tabs, currentTab } = useTabs(); + + if (!currentTab) { + return null; + } + + return ( +
+ + +
+ ); +} + +function TabsHeader({ tabs }: { tabs: Tab[] }) { + const { select, close, reorder } = useTabs(); + + return ( +
+ + {tabs.map((tab) => ( + + + + ))} + +
+ ); +} + +function TabItem( + { tab, handleClose, handleSelect }: { tab: Tab; handleClose: (tab: Tab) => void; handleSelect: (tab: Tab) => void }, +) { + if (tab.type === "sessions") { + return ; + } + + if (tab.type === "events") { + return ; + } + + if (tab.type === "calendars") { + return ; + } + if (tab.type === "folders") { + return ; + } + + if (tab.type === "humans") { + return ; + } + + return null; +} + +function TabContent({ tab }: { tab: Tab }) { + if (tab.type === "sessions") { + return ; + } + + if (tab.type === "events") { + return ; + } + + if (tab.type === "calendars") { + return ; + } + + if (tab.type === "folders") { + return ; + } + + if (tab.type === "humans") { + return ; + } + + return null; +} diff --git a/apps/desktop2/src/components/main/body/sessions.tsx b/apps/desktop2/src/components/main/body/sessions.tsx new file mode 100644 index 0000000000..12cbf86ddd --- /dev/null +++ b/apps/desktop2/src/components/main/body/sessions.tsx @@ -0,0 +1,143 @@ +import { StickyNoteIcon } from "lucide-react"; +import { useCallback } from "react"; + +import NoteEditor from "@hypr/tiptap/editor"; +import { TabHeader } from "@hypr/ui/components/block/tab-header"; +import TitleInput from "@hypr/ui/components/block/title-input"; +import * as persisted from "../../../store/tinybase/persisted"; +import { rowIdfromTab, type Tab, useTabs } from "../../../store/zustand/tabs"; +import { type TabItem, TabItemBase } from "./shared"; + +export const TabItemNote: TabItem = ({ tab, handleClose, handleSelect }) => { + const title = persisted.UI.useCell("sessions", rowIdfromTab(tab), "title", persisted.STORE_ID); + + return ( + } + title={title ?? ""} + active={tab.active} + handleClose={() => handleClose(tab)} + handleSelect={() => handleSelect(tab)} + /> + ); +}; + +export function TabContentNote({ tab }: { tab: Tab }) { + const sessionId = rowIdfromTab(tab); + const sessionRow = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); + + const handleEditTitle = persisted.UI.useSetRowCallback( + "sessions", + sessionId, + (input: string, _store) => ({ ...sessionRow, title: input }), + [sessionRow], + persisted.STORE_ID, + ); + + const handleEditRawMd = persisted.UI.useSetRowCallback( + "sessions", + sessionId, + (input: string, _store) => ({ ...sessionRow, raw_md: input }), + [sessionRow], + persisted.STORE_ID, + ); + + return ( +
+ + + handleEditTitle(e.target.value)} + /> + {}} + currentTab="raw" + onTabChange={() => {}} + isCurrentlyRecording={false} + shouldShowTab={true} + shouldShowEnhancedTab={false} + /> + handleEditRawMd(e)} + mentionConfig={{ + trigger: "@", + handleSearch: async () => { + return []; + }, + }} + /> +
+ ); +} + +function TabContentNoteHeader({ sessionRow }: { sessionRow: ReturnType> }) { + return ( +
+
+ {sessionRow.folder_id && ( + + )} +
+ + {sessionRow.event_id && } +
+ ); +} + +function TabContentNoteHeaderFolderChain({ title, folderId }: { title: string; folderId: string }) { + const folderIds = persisted.UI.useLinkedRowIds( + "folderToParentFolder", + folderId, + persisted.STORE_ID, + ); + + if (!folderIds || folderIds.length === 0) { + return null; + } + + const folderChain = [...folderIds].reverse(); + + return ( +
+ {folderChain.map((id, index) => ( +
+ {index > 0 && /} + +
+ ))} +
+ / + {title} +
+
+ ); +} + +function TabContentNoteHeaderFolder({ folderId }: { folderId: string }) { + const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID); + const { openNew } = useTabs(); + const handleClick = useCallback(() => { + openNew({ type: "folders", id: folderId, active: true }); + }, [openNew, folderId]); + + return ( + + ); +} + +function TabContentNoteHeaderEvent({ eventId }: { eventId: string }) { + const eventRow = persisted.UI.useRow("events", eventId, persisted.STORE_ID); + return
{eventRow.title}
; +} diff --git a/apps/desktop2/src/components/main/body/shared.tsx b/apps/desktop2/src/components/main/body/shared.tsx new file mode 100644 index 0000000000..ffda0bf59b --- /dev/null +++ b/apps/desktop2/src/components/main/body/shared.tsx @@ -0,0 +1,52 @@ +import { clsx } from "clsx"; + +import { type Tab } from "../../../store/zustand/tabs"; + +export type TabItem = (props: { + tab: Tab; + handleClose: (tab: Tab) => void; + handleSelect: (tab: Tab) => void; +}) => React.ReactNode; + +export function TabItemBase( + { icon, title, active, handleClose, handleSelect }: { + icon: React.ReactNode; + title: string; + active: boolean; + handleClose: () => void; + handleSelect: () => void; + }, +) { + return ( +
+ + +
+ ); +} diff --git a/apps/desktop2/src/components/main/left-sidebar.tsx b/apps/desktop2/src/components/main/left-sidebar.tsx deleted file mode 100644 index 42fb0446d9..0000000000 --- a/apps/desktop2/src/components/main/left-sidebar.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { clsx } from "clsx"; -import { PanelLeftCloseIcon } from "lucide-react"; -import { useCell, useRowIds } from "tinybase/ui-react"; - -import * as persisted from "../../store/tinybase/persisted"; - -import { ContextMenuItem } from "@hypr/ui/components/ui/context-menu"; -import { useLeftSidebar } from "@hypr/utils/contexts"; -import { useTabs } from "../../store/zustand/tabs"; -import { type Tab } from "../../store/zustand/tabs"; -import { InteractiveButton } from "../interactive-button"; - -export function LeftSidebar() { - const { togglePanel: toggleLeftPanel } = useLeftSidebar(); - - return ( -
-
- -
- - -
- ); -} - -function TimelineView() { - const allSessionIds = useRowIds("sessions", persisted.STORE_ID); - const { currentTab } = useTabs(); - - return ( -
- {allSessionIds?.map((sessionId) => ( - - ))} -
- ); -} - -function SessionItem({ sessionId, active }: { sessionId: string; active?: boolean }) { - const title = useCell("sessions", sessionId, "title", persisted.STORE_ID); - const tab: Tab = { id: sessionId, type: "sessions", active: false }; - - const { openCurrent, openNew } = useTabs(); - - const contextMenu = ( - <> - console.log("Delete session:", sessionId)}> - Delete - - - ); - - return ( - openCurrent(tab)} - onCmdClick={() => openNew(tab)} - contextMenu={contextMenu} - className={clsx([ - "w-full text-left px-2 py-1 hover:bg-blue-50 border-b border-gray-100", - active && "bg-blue-50", - ])} - > -
{title}
-
- ); -} diff --git a/apps/desktop2/src/components/main/main-area.tsx b/apps/desktop2/src/components/main/main-area.tsx deleted file mode 100644 index 05f59b6bec..0000000000 --- a/apps/desktop2/src/components/main/main-area.tsx +++ /dev/null @@ -1,712 +0,0 @@ -import { useRouteContext } from "@tanstack/react-router"; -import { clsx } from "clsx"; -import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameMonth, startOfMonth } from "date-fns"; -import { - CalendarIcon, - CogIcon, - FileTextIcon, - FolderIcon, - PanelLeftOpenIcon, - PencilIcon, - StickyNoteIcon, -} from "lucide-react"; -import { Reorder } from "motion/react"; -import { useCallback } from "react"; - -import { commands as windowsCommands } from "@hypr/plugin-windows"; -import NoteEditor from "@hypr/tiptap/editor"; -import { CalendarStructure } from "@hypr/ui/components/block/calendar-structure"; -import { TabHeader } from "@hypr/ui/components/block/tab-header"; -import TitleInput from "@hypr/ui/components/block/title-input"; -import { useLeftSidebar } from "@hypr/utils/contexts"; -import * as persisted from "../../store/tinybase/persisted"; -import { useTabs } from "../../store/zustand/tabs"; -import { rowIdfromTab, type Tab, uniqueIdfromTab } from "../../store/zustand/tabs"; - -export function MainContent() { - const { tabs, currentTab } = useTabs(); - - if (!currentTab) { - return null; - } - - return ( -
- - -
- ); -} - -export function MainHeader() { - const { persistedStore, internalStore } = useRouteContext({ from: "__root__" }); - const { isExpanded: isLeftPanelExpanded, togglePanel: toggleLeftPanel } = useLeftSidebar(); - const { openNew } = useTabs(); - - const handleClickSettings = useCallback(() => { - windowsCommands.windowShow({ type: "settings" }); - }, []); - - const handleClickNewNote = useCallback(() => { - if (!persistedStore || !internalStore) { - return; - } - - const sessionId = crypto.randomUUID(); - const user_id = internalStore.getValue("user_id"); - - persistedStore.setRow("sessions", sessionId, { - title: "new", - user_id, - created_at: new Date().toISOString(), - }); - openNew({ id: sessionId, type: "sessions", active: true }); - }, [persistedStore, internalStore, openNew]); - - return ( -
- {!isLeftPanelExpanded - && ( - - )} - -
- - - openNew({ type: "folders", id: null, active: true })} - className="cursor-pointer h-5 w-5 text-muted-foreground hover:text-foreground" - /> - - openNew({ type: "calendars", month: new Date(), active: true })} - className="cursor-pointer h-5 w-5 text-muted-foreground hover:text-foreground" - /> -
-
- ); -} - -function TabsHeader({ tabs }: { tabs: Tab[] }) { - const { select, close, reorder } = useTabs(); - - return ( -
- - {tabs.map((tab) => ( - - - - ))} - -
- ); -} - -function TabItem( - { tab, handleClose, handleSelect }: { tab: Tab; handleClose: (tab: Tab) => void; handleSelect: (tab: Tab) => void }, -) { - if (tab.type === "sessions") { - return ; - } - - if (tab.type === "events") { - return ; - } - - if (tab.type === "calendars") { - return ; - } - if (tab.type === "folders") { - return ; - } - - return null; -} - -const TabItemNote: TabItem = ({ tab, handleClose, handleSelect }) => { - const title = persisted.UI.useCell("sessions", rowIdfromTab(tab), "title", persisted.STORE_ID); - - return ( - } - title={title ?? ""} - active={tab.active} - handleClose={() => handleClose(tab)} - handleSelect={() => handleSelect(tab)} - /> - ); -}; - -const TabItemEvent: TabItem = ({ tab, handleClose, handleSelect }) => { - const title = persisted.UI.useCell("events", rowIdfromTab(tab), "title", persisted.STORE_ID); - - return ( - } - title={title ?? ""} - active={tab.active} - handleClose={() => handleClose(tab)} - handleSelect={() => handleSelect(tab)} - /> - ); -}; - -const TabItemCalendar: TabItem = ({ tab, handleClose, handleSelect }) => { - return ( - } - title={"Calendar"} - active={tab.active} - handleClose={() => handleClose(tab)} - handleSelect={() => handleSelect(tab)} - /> - ); -}; - -const TabItemFolder: TabItem = ({ tab, handleClose, handleSelect }) => { - if (tab.type === "folders" && tab.id === null) { - return ; - } - - if (tab.type === "folders" && tab.id !== null) { - return ; - } - - return null; -}; - -const TabItemFolderAll: TabItem = ({ tab, handleClose, handleSelect }) => { - return ( - } - title={"Folder"} - active={tab.active} - handleClose={() => handleClose(tab)} - handleSelect={() => handleSelect(tab)} - /> - ); -}; - -const TabItemFolderSpecific: TabItem = ({ tab, handleClose, handleSelect }) => { - if (tab.type !== "folders" || tab.id === null) { - return null; - } - - const folderName = persisted.UI.useCell("folders", tab.id, "name", persisted.STORE_ID); - - return ( - } - title={folderName ?? ""} - active={tab.active} - handleClose={() => handleClose(tab)} - handleSelect={() => handleSelect(tab)} - /> - ); -}; - -type TabItem = (props: { - tab: Tab; - handleClose: (tab: Tab) => void; - handleSelect: (tab: Tab) => void; -}) => React.ReactNode; - -function TabItemBase( - { icon, title, active, handleClose, handleSelect }: { - icon: React.ReactNode; - title: string; - active: boolean; - handleClose: () => void; - handleSelect: () => void; - }, -) { - return ( -
- - -
- ); -} - -function TabContent({ tab }: { tab: Tab }) { - if (tab.type === "sessions") { - return ; - } - - if (tab.type === "events") { - return ; - } - - if (tab.type === "calendars") { - return ; - } - - if (tab.type === "folders") { - return ; - } - - return null; -} - -function TabContentNote({ tab }: { tab: Tab }) { - const sessionId = rowIdfromTab(tab); - const sessionRow = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); - - const handleEditTitle = persisted.UI.useSetRowCallback( - "sessions", - sessionId, - (input: string, _store) => ({ ...sessionRow, title: input }), - [sessionRow], - persisted.STORE_ID, - ); - - const handleEditRawMd = persisted.UI.useSetRowCallback( - "sessions", - sessionId, - (input: string, _store) => ({ ...sessionRow, raw_md: input }), - [sessionRow], - persisted.STORE_ID, - ); - - return ( -
- - handleEditTitle(e.target.value)} - /> - {}} - currentTab="raw" - onTabChange={() => {}} - isCurrentlyRecording={false} - shouldShowTab={true} - shouldShowEnhancedTab={false} - /> - handleEditRawMd(e)} - mentionConfig={{ - trigger: "@", - handleSearch: async () => { - return []; - }, - }} - /> -
- ); -} - -function TabContentNoteHeader({ sessionRow }: { sessionRow: ReturnType> }) { - return ( -
-
- {sessionRow.folder_id && ( - - )} -
- - {sessionRow.event_id && } -
- ); -} - -function TabContentNoteHeaderFolderChain({ title, folderId }: { title: string; folderId: string }) { - const folderIds = persisted.UI.useLinkedRowIds( - "folderToParentFolder", - folderId, - persisted.STORE_ID, - ); - - if (!folderIds || folderIds.length === 0) { - return null; - } - - const folderChain = [...folderIds].reverse(); - - return ( -
- {folderChain.map((id, index) => ( -
- {index > 0 && /} - -
- ))} -
- / - {title} -
-
- ); -} - -function TabContentNoteHeaderFolder({ folderId }: { folderId: string }) { - const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID); - return {folderName}; -} - -function TabContentNoteHeaderEvent({ eventId }: { eventId: string }) { - const eventRow = persisted.UI.useRow("events", eventId, persisted.STORE_ID); - return
{eventRow.title}
; -} - -function TabContentEvent({ tab }: { tab: Tab }) { - const id = rowIdfromTab(tab); - const event = persisted.UI.useRow("events", id, persisted.STORE_ID); - - return
{JSON.stringify(event, null, 2)}
; -} - -function TabContentCalendar({ tab }: { tab: Tab }) { - if (tab.type !== "calendars") { - return null; - } - - const { openCurrent } = useTabs(); - const monthStart = startOfMonth(tab.month); - const monthEnd = endOfMonth(tab.month); - const days = eachDayOfInterval({ start: monthStart, end: monthEnd }).map((day) => format(day, "yyyy-MM-dd")); - const startDayOfWeek = getDay(monthStart); - const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - - const handlePreviousMonth = () => { - openCurrent({ ...tab, month: addMonths(tab.month, -1) }); - }; - - const handleNextMonth = () => { - openCurrent({ ...tab, month: addMonths(tab.month, 1) }); - }; - - const handleToday = () => { - openCurrent({ ...tab, month: new Date() }); - }; - - return ( - - {days.map((day) => ( - - ))} - - ); -} - -function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrentMonth: boolean }) { - const eventIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.eventsByDate, - day, - persisted.STORE_ID, - ); - - const sessionIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.sessionByDateWithoutEvent, - day, - persisted.STORE_ID, - ); - - const dayNumber = format(new Date(day), "d"); - const isToday = format(new Date(), "yyyy-MM-dd") === day; - const dayOfWeek = getDay(new Date(day)); - const isWeekend = dayOfWeek === 0 || dayOfWeek === 6; - - return ( -
-
-
-
- - {dayNumber} - -
-
-
- -
-
- {eventIds.map((eventId) => )} - {sessionIds.map((sessionId) => )} -
-
-
- ); -} - -function TabContentCalendarDayEvents({ eventId }: { eventId: string }) { - const event = persisted.UI.useRow("events", eventId, persisted.STORE_ID); - - return ( -
- -
- {event.title} -
-
- ); -} - -function TabContentCalendarDaySessions({ sessionId }: { sessionId: string }) { - const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); - return ( -
- -
- {session.title} -
-
- ); -} - -function TabContentFolder({ tab }: { tab: Tab }) { - if (tab.type !== "folders") { - return null; - } - - // If tab.id is null, show top-level folders - if (tab.id === null) { - return ; - } - - // If tab.id is a folder, show that folder's contents - return ; -} - -function TabContentFolderTopLevel() { - const topLevelFolderIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.foldersByParent, - "", - persisted.STORE_ID, - ); - - return ( -
-

All Folders

-
- {topLevelFolderIds?.map((folderId) => )} -
-
- ); -} - -function FolderCard({ folderId }: { folderId: string }) { - const folder = persisted.UI.useRow("folders", folderId, persisted.STORE_ID); - const { openCurrent } = useTabs(); - - // Count children - const childFolderIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.foldersByParent, - folderId, - persisted.STORE_ID, - ); - - const sessionIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.sessionsByFolder, - folderId, - persisted.STORE_ID, - ); - - const childCount = (childFolderIds?.length ?? 0) + (sessionIds?.length ?? 0); - - return ( -
openCurrent({ type: "folders", id: folderId, active: true })} - > - - {folder.name} - {childCount > 0 && {childCount} items} -
- ); -} - -function TabContentFolderSpecific({ folderId }: { folderId: string }) { - const childFolderIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.foldersByParent, - folderId, - persisted.STORE_ID, - ); - - const sessionIds = persisted.UI.useSliceRowIds( - persisted.INDEXES.sessionsByFolder, - folderId, - persisted.STORE_ID, - ); - - return ( -
- - - {(childFolderIds?.length ?? 0) > 0 && ( -
-

Folders

-
- {childFolderIds!.map((childId) => )} -
-
- )} - - {(sessionIds?.length ?? 0) > 0 && ( -
-

Notes

-
- {sessionIds!.map((sessionId) => )} -
-
- )} - - {(childFolderIds?.length ?? 0) === 0 && (sessionIds?.length ?? 0) === 0 && ( -
- This folder is empty -
- )} -
- ); -} - -function TabContentFolderBreadcrumb({ folderId }: { folderId: string }) { - const { openCurrent } = useTabs(); - - const folderIds = persisted.UI.useLinkedRowIds( - "folderToParentFolder", - folderId, - persisted.STORE_ID, - ); - - if (!folderIds || folderIds.length === 0) { - return null; - } - - const folderChain = [...folderIds].reverse(); - - return ( -
- - {folderChain.map((id) => { - const isLast = id === folderId; - return ( -
- / - -
- ); - })} -
- ); -} - -function TabContentFolderBreadcrumbItem({ folderId }: { folderId: string }) { - const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID); - return {folderName}; -} - -function FolderSessionItem({ sessionId }: { sessionId: string }) { - const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID); - const { openCurrent } = useTabs(); - - return ( -
openCurrent({ type: "sessions", id: sessionId, active: true })} - > - - {session.title} -
- ); -} diff --git a/apps/desktop2/src/components/main/sidebar/index.tsx b/apps/desktop2/src/components/main/sidebar/index.tsx new file mode 100644 index 0000000000..9679031f7e --- /dev/null +++ b/apps/desktop2/src/components/main/sidebar/index.tsx @@ -0,0 +1,32 @@ +import { clsx } from "clsx"; +import { PanelLeftCloseIcon } from "lucide-react"; + +import { useLeftSidebar } from "@hypr/utils/contexts"; +import { ProfileSection } from "./profile"; +import { TimelineView } from "./timeline"; + +export function LeftSidebar() { + const { togglePanel: toggleLeftPanel } = useLeftSidebar(); + + return ( +
+
+ +
+ + + +
+ ); +} diff --git a/apps/desktop2/src/components/main/sidebar/profile.tsx b/apps/desktop2/src/components/main/sidebar/profile.tsx new file mode 100644 index 0000000000..28bd8f222b --- /dev/null +++ b/apps/desktop2/src/components/main/sidebar/profile.tsx @@ -0,0 +1,97 @@ +import { clsx } from "clsx"; +import { Bell, Calendar, ChevronUpIcon, FolderOpen, RefreshCw, Settings, Users } from "lucide-react"; +import { useCallback, useState } from "react"; + +import { commands as windowsCommands } from "@hypr/plugin-windows"; +import { useTabs } from "../../../store/zustand/tabs"; + +export function ProfileSection() { + const [isExpanded, setIsExpanded] = useState(false); + const { openNew } = useTabs(); + + const handleClickSettings = useCallback(() => { + windowsCommands.windowShow({ type: "settings" }); + }, []); + + const handleClickFolders = useCallback(() => { + openNew({ type: "folders", id: null, active: true }); + }, [openNew]); + + const handleClickCalendar = useCallback(() => { + openNew({ type: "calendars", month: new Date(), active: true }); + }, [openNew]); + + const handleClickNotifications = useCallback(() => { + console.log("Notifications"); + }, []); + + const handleClickCheckUpdates = useCallback(() => { + console.log("Check for updates"); + }, []); + + const handleClickContacts = useCallback(() => { + console.log("Contacts"); + }, []); + + const menuItems = [ + { icon: Bell, label: "Notifications", badge: 10, onClick: handleClickNotifications }, + { icon: RefreshCw, label: "Check for updates", onClick: handleClickCheckUpdates }, + { icon: FolderOpen, label: "Folders", onClick: handleClickFolders }, + { icon: Users, label: "Contacts", onClick: handleClickContacts }, + { icon: Calendar, label: "Calendar", onClick: handleClickCalendar }, + { icon: Settings, label: "Settings", onClick: handleClickSettings }, + ]; + + return ( +
+
+
+ {menuItems.map((item) => ( + + ))} +
+
+ +
+
setIsExpanded(!isExpanded)} + > +
+ Profile +
+
+
John Jeong
+
+ +
+
+
+ ); +} diff --git a/apps/desktop2/src/components/main/sidebar/timeline.tsx b/apps/desktop2/src/components/main/sidebar/timeline.tsx new file mode 100644 index 0000000000..6d51f153d6 --- /dev/null +++ b/apps/desktop2/src/components/main/sidebar/timeline.tsx @@ -0,0 +1,52 @@ +import { clsx } from "clsx"; + +import { ContextMenuItem } from "@hypr/ui/components/ui/context-menu"; +import * as persisted from "../../../store/tinybase/persisted"; +import { Tab, useTabs } from "../../../store/zustand/tabs"; +import { InteractiveButton } from "../../interactive-button"; + +export function TimelineView() { + const allSessionIds = persisted.UI.useRowIds("sessions", persisted.STORE_ID); + const { currentTab } = useTabs(); + + return ( +
+ {allSessionIds?.map((sessionId) => ( + + ))} +
+ ); +} + +function SessionItem({ sessionId, active }: { sessionId: string; active?: boolean }) { + const title = persisted.UI.useCell("sessions", sessionId, "title", persisted.STORE_ID); + const tab: Tab = { id: sessionId, type: "sessions", active: false }; + + const { openCurrent, openNew } = useTabs(); + + const contextMenu = ( + <> + console.log("Delete session:", sessionId)}> + Delete + + + ); + + return ( + openCurrent(tab)} + onCmdClick={() => openNew(tab)} + contextMenu={contextMenu} + className={clsx([ + "w-full text-left px-2 py-1 hover:bg-blue-50 border-b border-gray-100", + active && "bg-blue-50", + ])} + > +
{title}
+
+ ); +} diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx index 99832cfc25..0af1c9199b 100644 --- a/apps/desktop2/src/routes/__root.tsx +++ b/apps/desktop2/src/routes/__root.tsx @@ -15,7 +15,7 @@ function Component() { - + diff --git a/apps/desktop2/src/routes/app/main/_layout.index.tsx b/apps/desktop2/src/routes/app/main/_layout.index.tsx index cb026cfb3c..42793059ed 100644 --- a/apps/desktop2/src/routes/app/main/_layout.index.tsx +++ b/apps/desktop2/src/routes/app/main/_layout.index.tsx @@ -3,8 +3,8 @@ import { createFileRoute } from "@tanstack/react-router"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@hypr/ui/components/ui/resizable"; import { useLeftSidebar } from "@hypr/utils/contexts"; import { FloatingChatButton } from "../../../components/floating-chat-button"; -import { LeftSidebar } from "../../../components/main/left-sidebar"; -import { MainContent, MainHeader } from "../../../components/main/main-area"; +import { MainContent } from "../../../components/main/body"; +import { LeftSidebar } from "../../../components/main/sidebar"; export const Route = createFileRoute("/app/main/_layout/")({ component: Component, @@ -24,10 +24,7 @@ function Component() { )} -
- - -
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01f3ca9dbd..8a376e3e36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9827,8 +9827,8 @@ packages: prosemirror-transform@1.10.4: resolution: {integrity: sha512-pwDy22nAnGqNR1feOQKHxoFkkUtepoFAd3r2hbEDsnf4wp57kKA36hXsB3njA9FtONBEwSDnDeCiJe+ItD+ykw==} - prosemirror-view@1.41.2: - resolution: {integrity: sha512-PGS/jETmh+Qjmre/6vcG7SNHAKiGc4vKOJmHMPRmvcUl7ISuVtrtHmH06UDUwaim4NDJfZfVMl7U7JkMMETa6g==} + prosemirror-view@1.41.3: + resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} @@ -16674,9 +16674,9 @@ snapshots: prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.3 prosemirror-tables: 1.8.1 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.2) + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3) prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 '@tiptap/react@3.6.5(@floating-ui/dom@1.7.4)(@tiptap/core@3.6.5(@tiptap/pm@3.6.5))(@tiptap/pm@3.6.5)(@types/react-dom@19.2.1(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: @@ -22604,20 +22604,20 @@ snapshots: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 prosemirror-gapcursor@1.3.2: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 prosemirror-history@1.4.1: dependencies: prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 rope-sequence: 1.3.4 prosemirror-inputrules@1.5.0: @@ -22661,7 +22661,7 @@ snapshots: dependencies: prosemirror-model: 1.25.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 prosemirror-tables@1.8.1: dependencies: @@ -22669,21 +22669,21 @@ snapshots: prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 prosemirror-transform: 1.10.4 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.2): + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.3)(prosemirror-state@1.4.3)(prosemirror-view@1.41.3): dependencies: '@remirror/core-constants': 3.0.0 escape-string-regexp: 4.0.0 prosemirror-model: 1.25.3 prosemirror-state: 1.4.3 - prosemirror-view: 1.41.2 + prosemirror-view: 1.41.3 prosemirror-transform@1.10.4: dependencies: prosemirror-model: 1.25.3 - prosemirror-view@1.41.2: + prosemirror-view@1.41.3: dependencies: prosemirror-model: 1.25.3 prosemirror-state: 1.4.3