Skip to content
528 changes: 0 additions & 528 deletions apps/desktop/src/components/main/body/calendars.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Checkbox } from "@hypr/ui/components/ui/checkbox";

import * as persisted from "../../../../store/tinybase/persisted";

export function CalendarCheckboxRow(
{ id, checked, onToggle }: { id: string; checked: boolean; onToggle: (checked: boolean) => void },
) {
const calendar = persisted.UI.useRow("calendars", id, persisted.STORE_ID);
return (
<div className="flex items-center space-x-2">
<Checkbox
id={`calendar-${id}`}
checked={checked}
onCheckedChange={(v) => onToggle(Boolean(v))}
/>
<label
htmlFor={`calendar-${id}`}
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 cursor-pointer"
>
{calendar?.name ?? "Untitled"}
</label>
</div>
);
}
143 changes: 143 additions & 0 deletions apps/desktop/src/components/main/body/calendars/calendar-day.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { cn, format, getDay } from "@hypr/utils";

import { useEffect, useRef, useState } from "react";

import * as persisted from "../../../../store/tinybase/persisted";
import { TabContentCalendarDayEvents } from "./day-events";
import { TabContentCalendarDayMore } from "./day-more";
import { TabContentCalendarDaySessions } from "./day-sessions";

export function TabContentCalendarDay({
day,
isCurrentMonth,
isFirstColumn,
isLastRow,
selectedCalendars,
}: {
day: string;
isCurrentMonth: boolean;
isFirstColumn: boolean;
isLastRow: boolean;
selectedCalendars: Set<string>;
}) {
const cellRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [maxVisibleItems, setMaxVisibleItems] = useState(5);

const allEventIds = persisted.UI.useSliceRowIds(
persisted.INDEXES.eventsByDate,
day,
persisted.STORE_ID,
);

const store = persisted.UI.useStore(persisted.STORE_ID);

const eventIds = allEventIds.filter((eventId) => {
const event = store?.getRow("events", eventId);
return event?.calendar_id && selectedCalendars.has(event.calendar_id as string);
});

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;

// Measure actual available height and calculate max visible items
useEffect(() => {
const measureHeight = () => {
if (cellRef.current && contentRef.current) {
const cellHeight = cellRef.current.clientHeight;
const contentTop = contentRef.current.offsetTop;
const availableHeight = cellHeight - contentTop;
const EVENT_HEIGHT = 20; // height of each event item (h-5)
const SPACING = 4; // space-y-1

// Calculate how many items can fit
const itemsWithSpacing = Math.floor((availableHeight + SPACING) / (EVENT_HEIGHT + SPACING));
// Reserve space for "+x more" if needed
setMaxVisibleItems(Math.max(1, itemsWithSpacing));
}
};

measureHeight();

// Re-measure on window resize
window.addEventListener("resize", measureHeight);
return () => window.removeEventListener("resize", measureHeight);
}, []);

const totalItems = eventIds.length + sessionIds.length;
const visibleCount = totalItems > maxVisibleItems
? maxVisibleItems - 1
: totalItems;
const hiddenCount = totalItems - visibleCount;

const allItems = [
...eventIds.map(id => ({ type: "event" as const, id })),
...sessionIds.map(id => ({ type: "session" as const, id })),
];

const visibleItems = allItems.slice(0, visibleCount);
const hiddenItems = allItems.slice(visibleCount);

const hiddenEventIds = hiddenItems
.filter(item => item.type === "event")
.map(item => item.id);
const hiddenSessionIds = hiddenItems
.filter(item => item.type === "session")
.map(item => item.id);

return (
<div
ref={cellRef}
className={cn([
"relative flex flex-col items-end flex-1 min-w-0 border-neutral-200 p-1 overflow-hidden",
!isFirstColumn && "border-l",
!isLastRow && "border-b",
isWeekend ? "bg-neutral-50" : "bg-white",
])}
>
<div
className={cn(
["text-sm size-6 rounded-full flex items-center justify-center mb-1", isToday && "bg-red-500"],
)}
>
<span
className={cn(
[
isToday && "text-white font-medium",
!isToday && !isCurrentMonth && "text-neutral-400",
!isToday && isCurrentMonth && isWeekend && "text-neutral-500",
!isToday && isCurrentMonth && !isWeekend && "text-neutral-700",
],
)}
>
{dayNumber}
</span>
</div>

<div ref={contentRef} className="flex-1 w-full">
{visibleItems.map((item) =>
item.type === "event"
? <TabContentCalendarDayEvents key={item.id} eventId={item.id} />
: <TabContentCalendarDaySessions key={item.id} sessionId={item.id} />
)}

{hiddenCount > 0 && (
<TabContentCalendarDayMore
day={day}
eventIds={hiddenEventIds}
sessionIds={hiddenSessionIds}
hiddenCount={hiddenCount}
/>
)}
</div>
</div>
);
}
91 changes: 91 additions & 0 deletions apps/desktop/src/components/main/body/calendars/day-events.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Button } from "@hypr/ui/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
import { cn, format, isSameDay } from "@hypr/utils";

import { Calendar, Pen, StickyNote } from "lucide-react";
import { useState } from "react";

import * as persisted from "../../../../store/tinybase/persisted";
import { useTabs } from "../../../../store/zustand/tabs";

export function TabContentCalendarDayEvents({ eventId }: { eventId: string }) {
const event = persisted.UI.useRow("events", eventId, persisted.STORE_ID);
const [open, setOpen] = useState(false);
const { openNew } = useTabs();

const title = event?.title || "Untitled Event";

const sessionIds = persisted.UI.useSliceRowIds(
persisted.INDEXES.sessionsByEvent,
eventId,
persisted.STORE_ID,
);
const linkedSessionId = sessionIds[0];
const linkedSession = persisted.UI.useRow("sessions", linkedSessionId || "dummy", persisted.STORE_ID);

const handleOpenNote = () => {
setOpen(false);

if (linkedSessionId) {
openNew({ type: "sessions", id: linkedSessionId, state: { editor: "raw" } });
} else {
openNew({ type: "sessions", id: crypto.randomUUID(), state: { editor: "raw" } });
}
};

const formatEventTime = () => {
if (!event || !event.started_at || !event.ended_at) {
return "";
}
const start = new Date(event.started_at);
const end = new Date(event.ended_at);

if (isNaN(start.getTime()) || isNaN(end.getTime())) {
return "";
}

if (isSameDay(start, end)) {
return `${format(start, "MMM d")}, ${format(start, "h:mm a")} - ${format(end, "h:mm a")}`;
}
return `${format(start, "MMM d")}, ${format(start, "h:mm a")} - ${format(end, "MMM d")}, ${format(end, "h:mm a")}`;
};

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
className={cn(
["w-full justify-start px-1 text-neutral-600 h-6", open && "bg-neutral-100 hover:bg-neutral-100"],
)}
>
<Calendar size={12} className="text-pink-600" />
<p className="truncate">{title}</p>
</Button>
</PopoverTrigger>
<PopoverContent className="w-72 p-4 bg-white border-neutral-200 m-2 shadow-lg outline-none focus:outline-none focus:ring-0">
<div className="font-semibold text-lg text-neutral-800 mb-2">
{title}
</div>

<p className="text-sm text-neutral-600 mb-4">
{formatEventTime()}
</p>

{linkedSessionId
? (
<Button className="w-full justify-start" onClick={handleOpenNote}>
<StickyNote />
<p className="truncate">{linkedSession?.title || "Untitled Note"}</p>
</Button>
)
: (
<Button className="w-full" onClick={handleOpenNote}>
<Pen />
Create Note
</Button>
)}
</PopoverContent>
</Popover>
);
}
45 changes: 45 additions & 0 deletions apps/desktop/src/components/main/body/calendars/day-more.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Button } from "@hypr/ui/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
import { format } from "@hypr/utils";

import { useState } from "react";

import { TabContentCalendarDayEvents } from "./day-events";
import { TabContentCalendarDaySessions } from "./day-sessions";

export function TabContentCalendarDayMore({
day,
eventIds,
sessionIds,
hiddenCount,
}: {
day: string;
eventIds: string[];
sessionIds: string[];
hiddenCount: number;
}) {
const [open, setOpen] = useState(false);

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" className="w-full justify-start px-1 text-neutral-600 h-6">
+{hiddenCount} more
</Button>
</PopoverTrigger>
<PopoverContent
className="w-80 p-4 max-h-96 space-y-2 overflow-y-auto bg-white border-neutral-200 m-2 shadow-lg outline-none focus:outline-none focus:ring-0"
align="start"
>
<div className="text-lg font-semibold text-neutral-800 mb-2">
{format(new Date(day), "MMMM d, yyyy")}
</div>

<div className="space-y-1">
{eventIds.map((eventId) => <TabContentCalendarDayEvents key={eventId} eventId={eventId} />)}
{sessionIds.map((sessionId) => <TabContentCalendarDaySessions key={sessionId} sessionId={sessionId} />)}
</div>
</PopoverContent>
</Popover>
);
}
25 changes: 25 additions & 0 deletions apps/desktop/src/components/main/body/calendars/day-sessions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Button } from "@hypr/ui/components/ui/button";

import { StickyNote } from "lucide-react";

import * as persisted from "../../../../store/tinybase/persisted";
import { useTabs } from "../../../../store/zustand/tabs";

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

const eventId = session?.event_id ?? "";
const event = persisted.UI.useRow("events", eventId, persisted.STORE_ID);

const handleClick = () => {
openNew({ type: "sessions", id: sessionId, state: { editor: "raw" } });
};

return (
<Button variant="ghost" className="w-full justify-start px-1 text-neutral-600 h-6" onClick={handleClick}>
<StickyNote size={12} className="text-blue-600" />
<p className="truncate">{event && eventId ? event.title : session?.title || "Untitled"}</p>
</Button>
);
}
Loading
Loading