Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions apps/desktop2/src/components/main/body/calendars.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TabItemBase
icon={<CalendarIcon className="w-4 h-4" />}
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() });
};
Comment on lines +22 to +44
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move useTabs before the type guard to satisfy the Hooks rule.

useTabs() currently sits after an early return, so when tab.type isn’t "calendars" the hook is skipped. If React later renders the same component with a calendar tab, the hook order changes and state can corrupt (the lint warning you’ve probably seen). Pull the hook call above the guard.

-export function TabContentCalendar({ tab }: { tab: Tab }) {
-  if (tab.type !== "calendars") {
-    return null;
-  }
-
-  const { openCurrent } = useTabs();
+export function TabContentCalendar({ tab }: { tab: Tab }) {
+  const { openCurrent } = useTabs();
+
+  if (tab.type !== "calendars") {
+    return null;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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() });
};
export function TabContentCalendar({ tab }: { tab: Tab }) {
const { openCurrent } = useTabs();
if (tab.type !== "calendars") {
return null;
}
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() });
};
// ...rest of component
}
🧰 Tools
🪛 Biome (2.1.2)

[error] 27-27: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

Hooks should not be called after an early return.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🤖 Prompt for AI Agents
In apps/desktop2/src/components/main/body/calendars.tsx around lines 22 to 44,
the call to useTabs() is after an early return guard for tab.type which violates
the Rules of Hooks; move the const { openCurrent } = useTabs(); call above the
if (tab.type !== "calendars") return null; so the hook is always invoked in the
same order, then keep the type guard and the rest of the month calculations and
handlers unchanged.


return (
<CalendarStructure
monthLabel={format(tab.month, "MMMM yyyy")}
weekDays={weekDays}
startDayOfWeek={startDayOfWeek}
onPreviousMonth={handlePreviousMonth}
onNextMonth={handleNextMonth}
onToday={handleToday}
>
{days.map((day) => (
<TabContentCalendarDay key={day} day={day} isCurrentMonth={isSameMonth(new Date(day), tab.month)} />
))}
</CalendarStructure>
);
}

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 (
<div
className={clsx([
"h-32 relative flex flex-col border-b border-neutral-200",
isWeekend ? "bg-neutral-50" : "bg-white",
])}
>
<div className="flex items-center justify-end px-1 text-sm h-8">
<div className={clsx("flex items-end gap-1", isToday && "items-center")}>
<div
className={clsx(
isToday && "bg-red-500 rounded-full w-6 h-6 flex items-center justify-center",
)}
>
<span
className={clsx(
isToday
? "text-white font-medium"
: !isCurrentMonth
? "text-neutral-400"
: isWeekend
? "text-neutral-500"
: "text-neutral-700",
)}
>
{dayNumber}
</span>
</div>
</div>
</div>

<div className="flex-1 overflow-hidden flex flex-col px-1">
<div className="space-y-1">
{eventIds.map((eventId) => <TabContentCalendarDayEvents key={eventId} eventId={eventId} />)}
{sessionIds.map((sessionId) => <TabContentCalendarDaySessions key={sessionId} sessionId={sessionId} />)}
</div>
</div>
</div>
);
}

function TabContentCalendarDayEvents({ eventId }: { eventId: string }) {
const event = persisted.UI.useRow("events", eventId, persisted.STORE_ID);

return (
<div className="flex items-center space-x-1 px-0.5 py-0.5 cursor-pointer rounded hover:bg-neutral-200 transition-colors h-5">
<CalendarIcon className="w-2.5 h-2.5 text-neutral-500 flex-shrink-0" />
<div className="flex-1 text-xs text-neutral-800 truncate">
{event.title}
</div>
</div>
);
}

function TabContentCalendarDaySessions({ sessionId }: { sessionId: string }) {
const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID);
return (
<div className="flex items-center space-x-1 px-0.5 py-0.5 cursor-pointer rounded hover:bg-neutral-200 transition-colors h-5">
<FileTextIcon className="w-2.5 h-2.5 text-neutral-500 flex-shrink-0" />
<div className="flex-1 text-xs text-neutral-800 truncate">
{session.title}
</div>
</div>
);
}
26 changes: 26 additions & 0 deletions apps/desktop2/src/components/main/body/events.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<TabItemBase
icon={<CalendarIcon className="w-4 h-4" />}
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 <pre>{JSON.stringify(event, null, 2)}</pre>;
}
215 changes: 215 additions & 0 deletions apps/desktop2/src/components/main/body/folders.tsx
Original file line number Diff line number Diff line change
@@ -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 <TabItemFolderAll tab={tab} handleClose={handleClose} handleSelect={handleSelect} />;
}

if (tab.type === "folders" && tab.id !== null) {
return <TabItemFolderSpecific tab={tab} handleClose={handleClose} handleSelect={handleSelect} />;
}

return null;
};

const TabItemFolderAll: TabItem = ({ tab, handleClose, handleSelect }) => {
return (
<TabItemBase
icon={<FolderIcon className="w-4 h-4" />}
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 (
<TabItemBase
icon={<FolderIcon className="w-4 h-4" />}
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 <TabContentFolderTopLevel />;
}

// If tab.id is a folder, show that folder's contents
return <TabContentFolderSpecific folderId={tab.id} />;
}

function TabContentFolderTopLevel() {
const topLevelFolderIds = persisted.UI.useSliceRowIds(
persisted.INDEXES.foldersByParent,
"",
persisted.STORE_ID,
);

return (
<div className="flex flex-col gap-4 p-4">
<h2 className="text-lg font-semibold">All Folders</h2>
<div className="grid grid-cols-4 gap-4">
{topLevelFolderIds?.map((folderId) => <FolderCard key={folderId} folderId={folderId} />)}
</div>
</div>
);
}

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 (
<div
className="flex flex-col items-center justify-center gap-2 p-6 border rounded-lg hover:bg-muted cursor-pointer"
onClick={() => openCurrent({ type: "folders", id: folderId, active: true })}
>
<FolderIcon className="w-12 h-12 text-muted-foreground" />
<span className="text-sm font-medium text-center">{folder.name}</span>
{childCount > 0 && <span className="text-xs text-muted-foreground">{childCount} items</span>}
</div>
);
}

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 (
<div className="flex flex-col gap-4 p-4">
<TabContentFolderBreadcrumb folderId={folderId} />

{(childFolderIds?.length ?? 0) > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Folders</h3>
<div className="grid grid-cols-4 gap-4">
{childFolderIds!.map((childId) => <FolderCard key={childId} folderId={childId} />)}
</div>
</div>
)}

{(sessionIds?.length ?? 0) > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Notes</h3>
<div className="space-y-2">
{sessionIds!.map((sessionId) => <FolderSessionItem key={sessionId} sessionId={sessionId} />)}
</div>
</div>
)}

{(childFolderIds?.length ?? 0) === 0 && (sessionIds?.length ?? 0) === 0 && (
<div className="text-center text-muted-foreground py-8">
This folder is empty
</div>
)}
</div>
);
}

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 (
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-2">
<button
onClick={() => openCurrent({ type: "folders", id: null, active: true })}
className="hover:text-foreground"
>
Root
</button>
{folderChain.map((id) => {
const isLast = id === folderId;
return (
<div key={id} className="flex items-center gap-2">
<span>/</span>
<button
onClick={() => !isLast && openCurrent({ type: "folders", id, active: true })}
className={isLast ? "text-foreground font-medium" : "hover:text-foreground"}
>
<TabContentFolderBreadcrumbItem folderId={id} />
</button>
</div>
);
})}
</div>
);
}

function TabContentFolderBreadcrumbItem({ folderId }: { folderId: string }) {
const folderName = persisted.UI.useCell("folders", folderId, "name", persisted.STORE_ID);
return <span>{folderName}</span>;
}

function FolderSessionItem({ sessionId }: { sessionId: string }) {
const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID);
const { openCurrent } = useTabs();

return (
<div
className="flex items-center gap-2 px-3 py-2 border rounded-md hover:bg-muted cursor-pointer"
onClick={() => openCurrent({ type: "sessions", id: sessionId, active: true })}
>
<StickyNoteIcon className="w-4 h-4 text-muted-foreground" />
<span className="text-sm">{session.title}</span>
</div>
);
}
Loading
Loading