diff --git a/apps/desktop/src/extension-globals.ts b/apps/desktop/src/extension-globals.ts index 29d42fe677..4b766d9a66 100644 --- a/apps/desktop/src/extension-globals.ts +++ b/apps/desktop/src/extension-globals.ts @@ -4,9 +4,15 @@ import * as jsxRuntime from "react/jsx-runtime"; import * as tinybaseUiReact from "tinybase/ui-react"; import * as Button from "@hypr/ui/components/ui/button"; +import * as ButtonGroup from "@hypr/ui/components/ui/button-group"; import * as Card from "@hypr/ui/components/ui/card"; +import * as Checkbox from "@hypr/ui/components/ui/checkbox"; +import * as Popover from "@hypr/ui/components/ui/popover"; import * as utils from "@hypr/utils"; +import * as main from "./store/tinybase/main"; +import { useTabs } from "./store/zustand/tabs"; + declare global { interface Window { __hypr_react: typeof React; @@ -14,6 +20,8 @@ declare global { __hypr_jsx_runtime: typeof jsxRuntime; __hypr_ui: Record; __hypr_utils: typeof utils; + __hypr_store: typeof main; + __hypr_tabs: { useTabs: typeof useTabs }; __hypr_tinybase_ui_react: typeof tinybaseUiReact; } } @@ -27,6 +35,12 @@ export function initExtensionGlobals() { window.__hypr_ui = { "components/ui/button": Button, + "components/ui/button-group": ButtonGroup, "components/ui/card": Card, + "components/ui/checkbox": Checkbox, + "components/ui/popover": Popover, }; + + window.__hypr_store = main; + window.__hypr_tabs = { useTabs }; } diff --git a/extensions/build.mjs b/extensions/build.mjs index b0584a7c1e..a03e1006ae 100644 --- a/extensions/build.mjs +++ b/extensions/build.mjs @@ -129,6 +129,14 @@ async function buildExtension(name) { path: args.path, namespace: "hypr-global", })); + build.onResolve({ filter: /^@hypr\/store$/ }, () => ({ + path: "@hypr/store", + namespace: "hypr-global", + })); + build.onResolve({ filter: /^@hypr\/tabs$/ }, () => ({ + path: "@hypr/tabs", + namespace: "hypr-global", + })); build.onResolve({ filter: /^tinybase\/ui-react$/ }, () => ({ path: "tinybase/ui-react", namespace: "hypr-global", @@ -175,6 +183,18 @@ async function buildExtension(name) { loader: "js", }; } + if (args.path === "@hypr/store") { + return { + contents: "module.exports = window.__hypr_store", + loader: "js", + }; + } + if (args.path === "@hypr/tabs") { + return { + contents: "module.exports = window.__hypr_tabs", + loader: "js", + }; + } if (args.path === "tinybase/ui-react") { return { contents: diff --git a/extensions/calendar/extension.json b/extensions/calendar/extension.json new file mode 100644 index 0000000000..84fe8c4799 --- /dev/null +++ b/extensions/calendar/extension.json @@ -0,0 +1,16 @@ +{ + "id": "calendar", + "name": "Calendar", + "version": "0.1.0", + "api_version": "0.1", + "description": "Calendar view extension for viewing events and sessions", + "entry": "main.js", + "panels": [ + { + "id": "calendar.main", + "title": "Calendar", + "entry": "dist/ui.js" + } + ], + "permissions": {} +} diff --git a/extensions/calendar/main.js b/extensions/calendar/main.js new file mode 100644 index 0000000000..85806c2b30 --- /dev/null +++ b/extensions/calendar/main.js @@ -0,0 +1,19 @@ +__hypr_extension.activate = function (context) { + hypr.log.info( + `Activating ${context.manifest.name} v${context.manifest.version}`, + ); + hypr.log.info(`Extension path: ${context.extensionPath}`); + hypr.log.info(`API version: ${context.manifest.api_version}`); +}; + +__hypr_extension.deactivate = function () { + hypr.log.info("Deactivating Calendar extension"); +}; + +__hypr_extension.getInfo = function () { + return { + name: "Calendar Extension", + version: "0.1.0", + description: "Calendar view extension for viewing events and sessions", + }; +}; diff --git a/extensions/calendar/ui.tsx b/extensions/calendar/ui.tsx new file mode 100644 index 0000000000..a78ab35fff --- /dev/null +++ b/extensions/calendar/ui.tsx @@ -0,0 +1,619 @@ +import { useEffect, useState } from "react"; + +import * as store from "@hypr/store"; +import { useTabs } from "@hypr/tabs"; +import { Button } from "@hypr/ui/components/ui/button"; +import { ButtonGroup } from "@hypr/ui/components/ui/button-group"; +import { Checkbox } from "@hypr/ui/components/ui/checkbox"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@hypr/ui/components/ui/popover"; +import { + addDays, + addMonths, + cn, + eachDayOfInterval, + format, + getDay, + isSameDay, + isSameMonth, + startOfMonth, + subDays, +} from "@hypr/utils"; + +export interface ExtensionViewProps { + extensionId: string; + state?: Record; +} + +export default function CalendarExtensionView({ + extensionId, + state, +}: ExtensionViewProps) { + const [month, setMonth] = useState(() => { + if (state?.month && typeof state.month === "string") { + return new Date(state.month); + } + return new Date(); + }); + const [sidebarOpen, setSidebarOpen] = useState(false); + + const calendarIds = store.UI.useRowIds("calendars", store.STORE_ID); + + const [selectedCalendars, setSelectedCalendars] = useState>( + () => new Set(calendarIds), + ); + + useEffect(() => { + setSelectedCalendars((prev) => { + const next = new Set(prev); + for (const id of calendarIds) { + if (!prev.has(id)) { + next.add(id); + } + } + return next; + }); + }, [calendarIds]); + + const monthStart = startOfMonth(month); + const startDayOfWeek = getDay(monthStart); + const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthLabel = format(month, "MMMM yyyy"); + + const calendarStart = subDays(monthStart, startDayOfWeek); + const totalCells = 42; + const calendarEnd = addDays(calendarStart, totalCells - 1); + const allDays = eachDayOfInterval({ + start: calendarStart, + end: calendarEnd, + }).map((day) => format(day, "yyyy-MM-dd")); + + const handlePreviousMonth = () => { + setMonth(addMonths(month, -1)); + }; + + const handleNextMonth = () => { + setMonth(addMonths(month, 1)); + }; + + const handleToday = () => { + setMonth(new Date()); + }; + + return ( +
+ {sidebarOpen && ( + + )} + +
+
+
+ {!sidebarOpen && ( + + )} +
+ {monthLabel} +
+ + + + + + + +
+ +
+ {weekDays.map((day, index) => ( +
+ {day} +
+ ))} +
+
+ +
+ {Array.from({ length: 6 }).map((_, weekIndex) => ( +
+ {allDays + .slice(weekIndex * 7, (weekIndex + 1) * 7) + .map((day, dayIndex) => ( + + ))} +
+ ))} +
+
+
+ ); +} + +function CalendarCheckboxRow({ + id, + checked, + onToggle, +}: { + id: string; + checked: boolean; + onToggle: (checked: boolean) => void; +}) { + const calendar = store.UI.useRow("calendars", id, store.STORE_ID); + return ( +
+ onToggle(Boolean(v))} + /> + +
+ ); +} + +function CalendarDay({ + day, + isCurrentMonth, + isFirstColumn, + isLastRow, + selectedCalendars, +}: { + day: string; + isCurrentMonth: boolean; + isFirstColumn: boolean; + isLastRow: boolean; + selectedCalendars: Set; +}) { + const allEventIds = store.UI.useSliceRowIds( + store.INDEXES.eventsByDate, + day, + store.STORE_ID, + ); + + const storeInstance = store.UI.useStore(store.STORE_ID); + + const eventIds = allEventIds.filter((eventId) => { + const event = storeInstance?.getRow("events", eventId); + return ( + event?.calendar_id && selectedCalendars.has(event.calendar_id as string) + ); + }); + + const sessionIds = store.UI.useSliceRowIds( + store.INDEXES.sessionByDateWithoutEvent, + day, + store.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; + + const totalItems = eventIds.length + sessionIds.length; + const maxVisibleItems = 3; + 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 ( +
+
+ + {dayNumber} + +
+ +
+ {visibleItems.map((item) => + item.type === "event" ? ( + + ) : ( + + ), + )} + + {hiddenCount > 0 && ( + + )} +
+
+ ); +} + +function DayEvent({ eventId }: { eventId: string }) { + const event = store.UI.useRow("events", eventId, store.STORE_ID); + const [open, setOpen] = useState(false); + const openNew = useTabs((state) => state.openNew); + + const title = event?.title || "Untitled Event"; + + const sessionIds = store.UI.useSliceRowIds( + store.INDEXES.sessionsByEvent, + eventId, + store.STORE_ID, + ); + const linkedSessionId = sessionIds[0]; + const linkedSession = store.UI.useRow( + "sessions", + linkedSessionId || "dummy", + store.STORE_ID, + ); + + const handleOpenNote = () => { + setOpen(false); + + if (linkedSessionId) { + openNew({ type: "sessions", id: linkedSessionId }); + } else { + openNew({ type: "sessions", id: crypto.randomUUID() }); + } + }; + + const formatEventTime = () => { + if (!event || !event.started_at || !event.ended_at) { + return ""; + } + const start = new Date(event.started_at as string); + const end = new Date(event.ended_at as string); + + 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 ( + + + + + +
+ {title} +
+ +

{formatEventTime()}

+ + {linkedSessionId ? ( + + ) : ( + + )} +
+
+ ); +} + +function DaySession({ sessionId }: { sessionId: string }) { + const session = store.UI.useRow("sessions", sessionId, store.STORE_ID); + const openNew = useTabs((state) => state.openNew); + + const eventId = session?.event_id ?? ""; + const event = store.UI.useRow("events", eventId as string, store.STORE_ID); + + const handleClick = () => { + openNew({ type: "sessions", id: sessionId }); + }; + + return ( + + ); +} + +function DayMore({ + day, + eventIds, + sessionIds, + hiddenCount, +}: { + day: string; + eventIds: string[]; + sessionIds: string[]; + hiddenCount: number; +}) { + const [open, setOpen] = useState(false); + + return ( + + + + + +
+ {format(new Date(day), "MMMM d, yyyy")} +
+ +
+ {eventIds.map((eventId) => ( + + ))} + {sessionIds.map((sessionId) => ( + + ))} +
+
+
+ ); +} + +function CalendarIcon() { + return ( + + + + + + + ); +} + +function CalendarDaysIcon() { + return ( + + + + + + + + + + + + + ); +} + +function ChevronLeftIcon() { + return ( + + + + ); +} + +function ChevronRightIcon() { + return ( + + + + ); +} + +function StickyNoteIcon() { + return ( + + + + + ); +} + +function PenIcon() { + return ( + + + + ); +} diff --git a/extensions/package.json b/extensions/package.json index d1821b80f7..32216086a6 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -6,6 +6,7 @@ "scripts": { "build": "node build.mjs build", "build:hello-world": "node build.mjs build hello-world", + "build:calendar": "node build.mjs build calendar", "clean": "node build.mjs clean", "install:dev": "node build.mjs install" },