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
238 changes: 223 additions & 15 deletions apps/desktop2/src/components/main/body/calendars.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { clsx } from "clsx";
import { addMonths, eachDayOfInterval, endOfMonth, format, getDay, isSameMonth, startOfMonth } from "date-fns";
import { CalendarIcon, FileTextIcon } from "lucide-react";
import { CalendarIcon, FileTextIcon, Pen } from "lucide-react";
import { useState } from "react";

import { CalendarStructure } from "@hypr/ui/components/block/calendar-structure";
import { Popover, PopoverContent, PopoverTrigger } from "@hypr/ui/components/ui/popover";
import * as persisted from "../../../store/tinybase/persisted";
import { type Tab, useTabs } from "../../../store/zustand/tabs";
import { type TabItem, TabItemBase } from "./shared";
Expand Down Expand Up @@ -77,6 +79,33 @@ function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrent
const dayOfWeek = getDay(new Date(day));
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;

const HEADER_HEIGHT = 32;
const EVENT_HEIGHT = 20;
const CELL_HEIGHT = 128;
const availableHeight = CELL_HEIGHT - HEADER_HEIGHT;
const maxPossibleEvents = Math.floor(availableHeight / EVENT_HEIGHT);

const totalItems = eventIds.length + sessionIds.length;
const visibleCount = totalItems > maxPossibleEvents
? maxPossibleEvents - 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
className={clsx([
Expand Down Expand Up @@ -110,8 +139,20 @@ function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrent

<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} />)}
{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>
</div>
Expand All @@ -120,25 +161,192 @@ function TabContentCalendarDay({ day, isCurrentMonth }: { day: string; isCurrent

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

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 handleClick = () => {
setOpen(false);

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

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

const formatTime = (date: Date) => {
const hours = date.getHours();
const minutes = date.getMinutes();
const ampm = hours >= 12 ? "PM" : "AM";
const displayHours = hours % 12 || 12;
return `${displayHours}:${minutes.toString().padStart(2, "0")} ${ampm}`;
};

const formatDate = (date: Date) => {
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return `${months[date.getMonth()]} ${date.getDate()}`;
};

const isSameDay = start.toDateString() === end.toDateString();
if (isSameDay) {
return `${formatDate(start)}, ${formatTime(start)} - ${formatTime(end)}`;
}
return `${formatDate(start)}, ${formatTime(start)} - ${formatDate(end)}, ${formatTime(end)}`;
};

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>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<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>
</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">
{event.title || "Untitled Event"}
</div>

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

{linkedSessionId
? (
<div
className="flex items-center gap-2 px-2 py-1 bg-neutral-50 border border-neutral-200 rounded-md cursor-pointer hover:bg-neutral-100 transition-colors"
onClick={handleClick}
>
<FileTextIcon className="size-3 text-neutral-600 flex-shrink-0" />
<div className="text-xs font-medium text-neutral-800 truncate">
{linkedSession?.title || "Untitled Note"}
</div>
</div>
)
: (
<div
className="flex items-center gap-2 px-2 py-1 bg-neutral-50 border border-neutral-200 rounded-md cursor-pointer hover:bg-neutral-100 transition-colors"
onClick={handleClick}
>
<Pen className="size-3 text-neutral-600 flex-shrink-0" />
<div className="text-xs font-medium text-neutral-800 truncate">
Create Note
</div>
</div>
)}
</PopoverContent>
</Popover>
);
}

function TabContentCalendarDaySessions({ sessionId }: { sessionId: string }) {
const session = persisted.UI.useRow("sessions", sessionId, persisted.STORE_ID);
const [open, setOpen] = useState(false);
const { openNew } = useTabs();

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

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

const formatSessionTime = () => {
const created = new Date(session.created_at || "");
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const hours = created.getHours();
const minutes = created.getMinutes();
const ampm = hours >= 12 ? "PM" : "AM";
const displayHours = hours % 12 || 12;

return `Created: ${months[created.getMonth()]} ${created.getDate()}, ${displayHours}:${
minutes.toString().padStart(2, "0")
} ${ampm}`;
};

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>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<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>
</PopoverTrigger>
<PopoverContent className="w-72 p-4 bg-white border-neutral-200 m-2 shadow-lg outline-none focus:outline-none focus:ring-0">
<h3 className="font-semibold text-lg mb-2">
{event && session.event_id ? event.title : session.title || "Untitled"}
</h3>

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

<div
className="flex items-center gap-2 px-2 py-1 bg-neutral-50 border border-neutral-200 rounded-md cursor-pointer hover:bg-neutral-100 transition-colors"
onClick={handleClick}
>
<FileTextIcon className="size-3 text-neutral-600 flex-shrink-0" />
<div className="text-xs font-medium text-neutral-800 truncate">
{event && session.event_id ? event.title : session.title || "Untitled"}
</div>
</div>
</PopoverContent>
</Popover>
);
}

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>
<div className="text-xs text-neutral-600 rounded py-0.5 cursor-pointer hover:bg-neutral-200 px-0.5 h-5">
+{hiddenCount} more
</div>
</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>
);
}
12 changes: 1 addition & 11 deletions apps/desktop2/src/components/main/body/contacts/details.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Building2, CircleMinus, FileText, Pencil, SearchIcon, TrashIcon, UserPlus } from "lucide-react";
import { Building2, CircleMinus, FileText, Pencil, SearchIcon, TrashIcon } from "lucide-react";
import React, { useState } from "react";

import { Button } from "@hypr/ui/components/ui/button";
Expand Down Expand Up @@ -65,16 +65,6 @@ export function DetailsColumn({
<p className="text-sm text-neutral-500">{selectedPersonData.email}</p>
)}
{selectedPersonData.org_id && <OrganizationInfo organizationId={selectedPersonData.org_id} />}
{!selectedPersonData.is_user && selectedPersonData.email && (
<button
onClick={() => {
}}
className="mt-2 inline-flex items-center gap-1 text-xs text-blue-600 hover:text-blue-700 hover:underline transition-colors cursor-pointer"
>
<UserPlus className="h-3 w-3" />
Recommend Hyprnote to {selectedPersonData.name}
</button>
)}
</div>
<div className="flex gap-2">
<button
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop2/src/store/tinybase/persisted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@ export const StoreComponent = () => {
(a, b) => a.localeCompare(b),
(a, b) => String(a).localeCompare(String(b)),
)
.setIndexDefinition(INDEXES.sessionsByEvent, "sessions", "event_id", "created_at")
.setIndexDefinition(INDEXES.tagsByName, "tags", "name")
.setIndexDefinition(INDEXES.tagSessionsBySession, "mapping_tag_session", "session_id")
.setIndexDefinition(INDEXES.tagSessionsByTag, "mapping_tag_session", "tag_id")
Expand Down Expand Up @@ -508,6 +509,7 @@ export const INDEXES = {
eventsByCalendar: "eventsByCalendar",
eventsByDate: "eventsByDate",
sessionByDateWithoutEvent: "sessionByDateWithoutEvent",
sessionsByEvent: "sessionsByEvent",
tagsByName: "tagsByName",
tagSessionsBySession: "tagSessionsBySession",
tagSessionsByTag: "tagSessionsByTag",
Expand Down
36 changes: 19 additions & 17 deletions packages/ui/src/components/block/calendar-structure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export const CalendarStructure = ({
children,
}: CalendarStructureProps) => {
return (
<div className="flex flex-col h-full p-4">
<div className="mb-4 flex items-center relative">
<div className="flex flex-col h-full">
<div className="p-4 pb-2 flex items-center relative">
<div className="text-xl font-semibold absolute left-1/2 transform -translate-x-1/2">{monthLabel}</div>
<div className="flex h-fit rounded-md overflow-clip border border-neutral-200 ml-auto">
<Button
Expand Down Expand Up @@ -50,21 +50,23 @@ export const CalendarStructure = ({
</Button>
</div>
</div>
<div className="grid grid-cols-7 divide-x divide-neutral-200">
{weekDays.map((day) => (
<div
key={day}
className="text-center text-sm font-medium text-muted-foreground p-2"
>
{day}
</div>
))}
</div>
<div className="grid grid-cols-7 divide-x divide-neutral-200 h-full grid-rows-6 gap-0">
{Array.from({ length: startDayOfWeek }).map((_, i) => (
<div key={`empty-${i}`} className="border-b border-neutral-200" />
))}
{children}
<div className="h-full">
<div className="grid grid-cols-7 border-b border-neutral-200">
{weekDays.map((day) => (
<div
key={day}
className="text-center text-sm font-medium text-muted-foreground p-2"
>
{day}
</div>
))}
</div>
<div className="grid grid-cols-7 divide-x divide-neutral-200 h-[calc(100%-48px)] grid-rows-6 gap-0">
{Array.from({ length: startDayOfWeek }).map((_, i) => (
<div key={`empty-${i}`} className="border-b border-neutral-200" />
))}
{children}
</div>
</div>
</div>
);
Expand Down
Loading