-
-
Notifications
You must be signed in to change notification settings - Fork 6
feat: Add calendar UI with place/time context integration #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Add timezone field to calendar_notes table - Create CalendarNotepadEnhanced component with time picker and timezone selector - Integrate Mapbox MCP for location geocoding - Implement standardized context format: lat, lng TIME (HH:MM:SS): DD/MM/YYYY Timezone - Add utility functions for formatting and parsing place/time context - Support 20+ common timezones with auto-detection - Update chat component to use enhanced calendar - Add comprehensive documentation Closes: Calendar place/time context feature request
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Manus AI seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
|
Warning Rate limit exceeded@ngoiyaeric has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 26 minutes and 53 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
WalkthroughThis PR introduces a Calendar UI Enhancement that extends the calendar notepad with time/timezone management and location tagging via Mapbox MCP. It adds a new Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant UI as CalendarNotepadEnhanced
participant Map as Mapbox MCP
participant DB as Database
participant Geocode as Geocoding Service
User->>UI: Select date, time, timezone
User->>UI: Click "Tag Location" button
alt Mapbox MCP Connected
UI->>Map: Request current map coordinates
Map-->>UI: Return lat/lng
UI->>Geocode: Reverse geocode coordinates
Geocode-->>UI: Return place name
UI->>UI: Append place name to note text
else Mapbox MCP Disconnected
UI->>UI: Append `#location` tag (coordinates only)
end
User->>UI: Type note content
User->>UI: Press Cmd/Ctrl+Enter
UI->>DB: Save note with timestamp, timezone, location metadata
DB-->>UI: Note saved
User->>UI: View note in list
UI->>UI: Render formatted place-time context
User->>UI: Click "Fly to location" button
UI->>Map: Pan/zoom to note coordinates
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
PR Compliance Guide 🔍Below is a summary of compliance checks for this PR:
Compliance status legend🟢 - Fully Compliant🟡 - Partial Compliant 🔴 - Not Compliant ⚪ - Requires Further Human Verification 🏷️ - Compliance label |
|||||||||||||||||||||||||
PR Code Suggestions ✨Explore these optional code suggestions:
|
|||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new calendar enhancement is functionally coherent and matches the documented place/time context format, but there are a few maintainability and robustness concerns. The heaviest issues are reliance on any for locationTags/taggedLocation and unguarded assumptions about GeoJSON coordinate structure, which could cause subtle runtime problems later. Timezone handling would benefit from a defensive fallback to uphold the documented "defaults to UTC" behavior across environments. The parsing utility is slightly brittle given its role as a documented API, and the main calendar component is large enough that some decomposition would improve long-term maintainability.
Additional notes (5)
- Maintainability |
components/calendar-notepad-enhanced.tsx:16-265
CalendarNotepadEnhancedhas grown to ~250 lines and mixes several concerns in a single component: date navigation, time selection, timezone selection, note CRUD, Mapbox integration, and rendering the note list. This makes it harder to test and evolve, especially as you add the "Future Enhancements" fromCALENDAR_ENHANCEMENT.md(recurring events, timezone conversions, etc.).
The logic is correct as-is, but a modest decomposition into smaller components/hooks would significantly improve readability and maintainability.
- Maintainability |
components/calendar-notepad-enhanced.tsx:23-23
getCurrentTimezone()is used here at state initialization time for a client component, which is fine, but it means the initial rendered timezone is determined once and never re-evaluated. If the user changes their system timezone while the app is open (or when hydrating from a server-rendered default), the selector won’t reflect that.
That may be acceptable for your UX, but if you intend “auto-detection” to be robust, consider explicitly re-checking on mount instead of baking the value at module import time in the utility, or at least clarifying the behavior.
- Readability |
components/calendar-notepad-enhanced.tsx:119-121
When tagging a location without a Mapbox MCP connection, you append" #location"to whatever text is already in the note. If the textarea is empty, this results in a note that starts with a leading space; if the user already typed trailing spaces or a#locationtag, this can produce redundant or messy content. Over multiple tag clicks, the suffix can accumulate.
Given you already store structured locationTags, the UX doesn’t need to rely on a textual #location marker and can be cleaner and idempotent.
- Maintainability |
lib/utils/calendar-context.ts:50-52
parsePlaceTimeContextrelies on a single, strict regex for the entire string (including the timezone). This is fine for internal round-tripping, but it will fail (returnnull) on seemingly minor variations, such as additional whitespace at the end, lowercasetime, or slightly different timezone tokens if the format ever evolves.
Given this is in a shared lib/utils module and explicitly documented as an API, a bit of normalization (e.g., trimming and case-insensitive TIME) would make it more robust and future-proof without adding much complexity.
- Maintainability |
lib/utils/calendar-context.ts:50-63
TheparsePlaceTimeContextregex assumes the timezone is any.+after the date, which is fine for simple IANA identifiers but will happily swallow trailing junk (e.g.Europe/Berlin foo) as part of the timezone. Since this parser might be reused for automation or integrations, accepting malformed tail content without validation can propagate bad data silently.
Given the documented format and the small, known set of timezones you support (COMMON_TIMEZONES), you can tighten this to trim surrounding whitespace and optionally validate that the parsed timezone matches one of your known values (or at least strip obvious trailing spaces).
Summary of changes
Summary of Changes
- Added
CALENDAR_ENHANCEMENT.mddocumenting the new calendar UI with place/time context, Mapbox MCP integration, and timezone behavior. - Introduced a new
CalendarNotepadEnhancedReact client component with time picker, timezone selector, location tagging, and Mapbox MCP geocoding integration. - Swapped the existing
CalendarNotepadusage incomponents/chat.tsxfor the newCalendarNotepadEnhancedin both mobile and desktop layouts. - Extended the
calendar_notesdatabase schema and Drizzle model with an optionaltimezonevarchar(100)column, including a SQL migration. - Updated the
CalendarNote/NewCalendarNotetypes to include a nullabletimezonefield. - Added
lib/utils/calendar-context.tsproviding formatting/parsing utilities for place/time context, GeoJSON point creation, current timezone detection, and a curated list of common timezones.
| const [dateOffset, setDateOffset] = useState(0) | ||
| const [notes, setNotes] = useState<CalendarNote[]>([]) | ||
| const [noteContent, setNoteContent] = useState('') | ||
| const [taggedLocation, setTaggedLocation] = useState<any>(null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
taggedLocation is typed as any, and downstream code assumes a GeoJSON Point shape (e.g., locationTags.coordinates and coordinates[0]/[1].toFixed(6)). This is an unsafe-but-type-valid pattern that can easily hide shape regressions or runtime errors if locationTags ever changes.
Given you already have a createGeoJSONPoint helper and a clear implicit type, this would benefit from a concrete GeoJSONPoint type (or reuse an existing one) to make the contract explicit and catch accidental misuse at compile time.
Suggestion
Consider introducing a dedicated GeoJSON point type (or importing one, e.g. from geojson), wiring it through CalendarNote.locationTags and taggedLocation, and using the existing createGeoJSONPoint helper to construct it instead of inline literals.
For example:
import type { Point } from 'geojson'
// lib/types/index.ts
export type CalendarNote = {
// ...
locationTags: Point | null;
// ...
}
// components/calendar-notepad-enhanced.tsx
const [taggedLocation, setTaggedLocation] = useState<Point | null>(null)
const handleTagLocation = async () => {
if (mapData.targetPosition) {
const [lng, lat] = mapData.targetPosition
setTaggedLocation(createGeoJSONPoint(lat, lng))
// ...
}
}
const handleFlyTo = (location: Point | null) => {
if (location?.coordinates) {
setMapData(prev => ({ ...prev, targetPosition: location.coordinates }))
}
}This keeps the data model consistent and lets TypeScript enforce that coordinates always exist and are numeric. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
| // Parse the selected time and combine with selected date | ||
| const [hours, minutes, seconds] = selectedTime.split(':').map(Number) | ||
| const noteDate = new Date(selectedDate) | ||
| noteDate.setHours(hours, minutes, seconds || 0) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleAddNote mutates the noteDate based on selectedTime.split(':') but doesn’t guard against invalid selectedTime values (e.g. empty string if the input is cleared). In that case, split(':') yields [''], Number('') is 0, and the note gets stored with 00:00:00, which is probably not what the user expects and might silently misrepresent the time.
Since time is a core part of the feature, it’s better to either prevent saving when the time is invalid or fall back to a sensible default explicitly (e.g. current time).
Suggestion
You can guard against invalid or empty selectedTime and make the behavior explicit:
const handleAddNote = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
if (!noteContent.trim()) return
let hours = 0, minutes = 0, seconds = 0
if (selectedTime) {
const parts = selectedTime.split(':').map(Number)
if (parts.length >= 2 && parts.every(n => Number.isFinite(n))) {
;[hours, minutes, seconds = 0] = parts
} else {
// fallback to current time if parsing fails
const now = new Date()
hours = now.getHours()
minutes = now.getMinutes()
seconds = now.getSeconds()
}
}
const noteDate = new Date(selectedDate)
noteDate.setHours(hours, minutes, seconds)
// ...rest unchanged
}
}This avoids silently writing 00:00:00 and documents the fallback behavior. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
| export function getCurrentTimezone(): string { | ||
| return Intl.DateTimeFormat().resolvedOptions().timeZone |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getCurrentTimezone() uses Intl.DateTimeFormat().resolvedOptions().timeZone directly. On modern browsers this is usually fine, but it can be undefined in some environments (e.g., some server-side runtimes, older browsers, or misconfigured locales). Since this value is persisted in the DB as timezone and used in formatted context strings, an explicit fallback would make the behavior more robust and predictable.
Right now, if timeZone is ever undefined, your component initial state and DB field could end up with an empty string or undefined depending on how the runtime behaves, which is subtly inconsistent with the documented "defaults to UTC" behavior.
Suggestion
Add a defensive fallback to 'UTC' when the browser does not expose a timezone, and keep the function resilient to being accidentally called in non-browser environments:
export function getCurrentTimezone(): string {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone
return tz || 'UTC'
} catch {
return 'UTC'
}
}This guarantees that timezone is always a sensible IANA identifier and matches the documentation that existing/unspecified notes default to UTC. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
| if (!note.locationTags || !note.locationTags.coordinates) { | ||
| return '' | ||
| } | ||
|
|
||
| const [lng, lat] = note.locationTags.coordinates | ||
| const date = new Date(note.date) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatPlaceTimeContext assumes note.locationTags.coordinates is an array with numeric values and calls toFixed(6) on them. If locationTags is malformed (e.g., user-provided JSON, legacy data, or a future schema change), this will throw at runtime.
The docstring and new migration indicate backward compatibility with existing notes, so it would be safer to guard against non-numeric coordinates rather than rely solely on upstream correctness.
Suggestion
Add a small runtime validation step before using coordinates, returning an empty string for malformed data so the UI remains resilient:
export function formatPlaceTimeContext(note: CalendarNote): string {
const coords = note.locationTags?.coordinates
if (!Array.isArray(coords) || coords.length < 2) return ''
const [lng, lat] = coords.map(Number)
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return ''
const date = new Date(note.date)
// ...rest of existing formatting logic
}This prevents runtime crashes on bad or legacy data while preserving the existing format for valid notes. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.
| const [lng, lat] = note.locationTags.coordinates | ||
| const date = new Date(note.date) | ||
|
|
||
| // Format time as HH:MM:SS | ||
| const hours = String(date.getHours()).padStart(2, '0') | ||
| const minutes = String(date.getMinutes()).padStart(2, '0') | ||
| const seconds = String(date.getSeconds()).padStart(2, '0') | ||
|
|
||
| // Format date as DD/MM/YYYY | ||
| const day = String(date.getDate()).padStart(2, '0') | ||
| const month = String(date.getMonth() + 1).padStart(2, '0') | ||
| const year = date.getFullYear() | ||
|
|
||
| const timezone = note.timezone || 'UTC' | ||
|
|
||
| return `${lat.toFixed(6)}, ${lng.toFixed(6)} TIME (${hours}:${minutes}:${seconds}): ${day}/${month}/${year} ${timezone}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You’re storing both date (with timezone) and a separate timezone string, but formatPlaceTimeContext ignores the timezone when computing the Date object; it just calls new Date(note.date) and uses local time methods (getHours(), etc.). This can produce misleading context strings if the server stores date in UTC while timezone represents a different user timezone—the displayed HH:MM:SS and DD/MM/YYYY will then be in the local environment timezone, not the stored timezone.
Given the feature’s emphasis on accurate place/time context, this mismatch can confuse users across timezones and diverge from what they expect when they pick a specific timezone in the UI.
Suggestion
If you need timezone-accurate formatting, consider using Intl.DateTimeFormat with the note.timezone value instead of raw Date methods, for example:
export function formatPlaceTimeContext(note: CalendarNote): string {
if (!note.locationTags || !note.locationTags.coordinates) return ''
const [lng, lat] = note.locationTags.coordinates
const date = new Date(note.date)
const timezone = note.timezone || 'UTC'
const timeFormatter = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
timeZone: timezone,
})
const dateFormatter = new Intl.DateTimeFormat('en-GB', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
timeZone: timezone,
})
const [time] = timeFormatter.formatToParts(date).reduce((acc, part) => {
if (part.type === 'hour') acc[0] = part.value
if (part.type === 'minute') acc[1] = part.value
if (part.type === 'second') acc[2] = part.value
return acc
}, ['00', '00', '00'] as string[])
const formattedTime = timeFormatter.format(date) // or rebuild from parts
const formattedDate = dateFormatter.format(date) // yields DD/MM/YYYY for 'en-GB'
// If you prefer strict control, you can derive day/month/year from `formatToParts` instead of relying on locale.
return `${lat.toFixed(6)}, ${lng.toFixed(6)} TIME (${formattedTime}): ${formattedDate} ${timezone}`
}You may want to centralize the timezone-aware formatting logic and reuse it both in the UI and utilities to avoid drift. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 8
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (7)
CALENDAR_ENHANCEMENT.md(1 hunks)components/calendar-notepad-enhanced.tsx(1 hunks)components/chat.tsx(3 hunks)drizzle/migrations/0002_add_timezone_to_calendar_notes.sql(1 hunks)lib/db/schema.ts(1 hunks)lib/types/index.ts(1 hunks)lib/utils/calendar-context.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
components/calendar-notepad-enhanced.tsx (5)
lib/types/index.ts (2)
CalendarNote(79-91)NewCalendarNote(93-93)lib/utils/calendar-context.ts (3)
getCurrentTimezone(89-91)COMMON_TIMEZONES(96-117)formatPlaceTimeContext(14-35)components/map/map-data-context.tsx (1)
useMapData(36-42)lib/actions/calendar.ts (2)
getNotes(16-57)saveNote(64-111)lib/utils/index.ts (1)
cn(11-13)
components/chat.tsx (1)
components/calendar-notepad-enhanced.tsx (1)
CalendarNotepadEnhanced(16-265)
lib/utils/calendar-context.ts (1)
lib/types/index.ts (1)
CalendarNote(79-91)
🪛 Biome (2.1.2)
components/calendar-notepad-enhanced.tsx
[error] 136-140: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
[error] 144-154: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
[error] 163-167: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
[error] 209-215: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
[error] 246-251: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🪛 markdownlint-cli2 (0.18.1)
CALENDAR_ENHANCEMENT.md
9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
14-14: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
21-21: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
21-21: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
26-26: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
26-26: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
35-35: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
107-107: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
135-135: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
🔇 Additional comments (8)
lib/db/schema.ts (1)
49-49: Timezone column addition is consistent and backward compatible
timezone: varchar('timezone', { length: 100 })cleanly aligns with the migration andCalendarNote['timezone'](string | null) while remaining nullable for existing rows. No issues from a schema perspective.lib/types/index.ts (1)
79-91: CalendarNote.timezone typing matches schema and utilitiesAdding
timezone: string | null(and propagating it viaNewCalendarNote) correctly mirrors the DB schema and supports helpers likeformatPlaceTimeContextthat default to'UTC'when null. This should keep calendar notes timezone-safe at the type level.drizzle/migrations/0002_add_timezone_to_calendar_notes.sql (1)
1-5: Migration is correct; confirm desired default semantics for timezoneThe migration cleanly adds a nullable
timezone VARCHAR(100)plus a clarifying COMMENT, matching the TS schema and utilities. Since there’s no DB-level default, the effective"defaults to UTC"behavior comes from code (e.g.note.timezone || 'UTC') rather than the database.If you ever want new rows to default at the DB layer instead of just at formatting time, you could change this to something like:
ALTER TABLE calendar_notes ADD COLUMN timezone VARCHAR(100) DEFAULT 'UTC';and update the Drizzle schema accordingly. Otherwise, the current approach is fine—just ensure the docs make it clear the default is applied in the formatter, not stored in the column.
components/chat.tsx (1)
9-9: CalendarNotepadEnhanced substitution and wiring look correctThe new
CalendarNotepadEnhancedimport and its usage in both mobile and desktop branches correctly passchatId={id}, keeping the existing chat scoping while enabling the enhanced calendar UI. No additional changes seem required here—just worth a quick smoke test on both layouts with the calendar toggle to confirm behavior.Also applies to: 101-101, 125-125
lib/utils/calendar-context.ts (4)
1-5: LGTM!Clean file structure with appropriate imports.
74-82: LGTM!Correct GeoJSON Point format with coordinates in [longitude, latitude] order as per the GeoJSON specification.
89-91: LGTM!Standard approach for retrieving the browser's IANA timezone identifier.
96-119: LGTM!Well-chosen timezone list with good global coverage and proper TypeScript const assertion for type safety.
| ### 1. Enhanced Time Management | ||
| - **Time Picker**: Select specific time (HH:MM:SS) for calendar notes | ||
| - **Timezone Support**: Choose from 20+ common timezones | ||
| - **Auto-detection**: Automatically detects user's current timezone | ||
|
|
||
| ### 2. Location Tagging with Mapbox MCP | ||
| - **Map Integration**: Tag notes with current map position | ||
| - **Geocoding**: Automatically fetches place names using Mapbox MCP | ||
| - **Visual Feedback**: Shows tagged coordinates in real-time | ||
|
|
||
| ### 3. Structured Context Format | ||
| All location-tagged notes display context in the standardized format: | ||
| ``` | ||
| latitude, longitude TIME (HH:MM:SS): DD/MM/YYYY Timezone | ||
| ``` | ||
|
|
||
| Example: | ||
| ``` | ||
| 40.758896, -73.985130 TIME (14:30:45): 20/11/2025 America/New_York | ||
| ``` | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Clean up markdownlint issues (headings, fenced blocks, and clarify UTC default wording)
A few small doc nits:
-
Add blank lines before/after headings like lines 9, 14, 19 to satisfy MD022.
-
Ensure fenced blocks are surrounded by blank lines (e.g., around the context-format example and TypeScript object snippet).
-
Add explicit languages to bare code fences:
- The context-format example and its example value can be tagged as
text:
- The context-format example and its example value can be tagged as
- -latitude, longitude TIME (HH:MM:SS): DD/MM/YYYY Timezone -
+text +latitude, longitude TIME (HH:MM:SS): DD/MM/YYYY Timezone +
- The parsed object example can be `ts` or `typescript`:
```diff
-```
+```ts
{
latitude: number
longitude: number
time: string // HH:MM:SS
date: string // DD/MM/YYYY
timezone: string
}
-```
+```
- In the “Compatibility” section, you mention “timezone defaults to UTC”. Right now that default is applied in code (e.g.,
note.timezone || 'UTC'when formatting the context string), not as a DB default. Consider rephrasing to something like “notes without an explicit timezone are displayed as UTC” to avoid implying a stored DB default.
Also applies to: 35-37, 107-115, 130-137
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
9-9: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
14-14: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
19-19: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
21-21: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
21-21: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
26-26: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
26-26: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
In CALENDAR_ENHANCEMENT.md around lines 9-29 (and also apply same fixes at
35-37, 107-115, 130-137), fix markdownlint issues by adding blank lines before
and after the headings at those line ranges, ensure all fenced code blocks are
surrounded by blank lines, and tag code fences with explicit languages: use
```text for the context-format example and its example value, and ```ts (or
```typescript) for the parsed object example; finally, reword the “timezone
defaults to UTC” phrasing in the Compatibility section to something like “notes
without an explicit timezone are displayed as UTC” to avoid implying a DB-level
default.
| const [selectedTimezone, setSelectedTimezone] = useState(getCurrentTimezone()) | ||
| const [isLoadingLocation, setIsLoadingLocation] = useState(false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ensure current timezone is present in the dropdown options
selectedTimezone is initialized with getCurrentTimezone(), but COMMON_TIMEZONES only includes a curated subset of zones. For users whose system timezone isn’t in that list (e.g., Europe/Madrid, America/Phoenix), the <select> will have a value not present in its <option>s, which can lead to confusing UI and React warnings.
A simple fix is to inject the current timezone into the options if it isn’t already present:
- const [selectedTimezone, setSelectedTimezone] = useState(getCurrentTimezone())
+ const [selectedTimezone, setSelectedTimezone] = useState(getCurrentTimezone())
@@
- const timezones = COMMON_TIMEZONES
+ const timezones =
+ COMMON_TIMEZONES.includes(selectedTimezone as any)
+ ? COMMON_TIMEZONES
+ : [selectedTimezone, ...COMMON_TIMEZONES]This keeps auto-detection accurate while still highlighting your curated list.
Also applies to: 131-132
| useEffect(() => { | ||
| const fetchNotes = async () => { | ||
| const fetchedNotes = await getNotes(selectedDate, chatId ?? null) | ||
| setNotes(fetchedNotes) | ||
| } | ||
| fetchNotes() | ||
| }, [selectedDate, chatId]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Avoid stale fetch results when rapidly changing selectedDate
The useEffect that calls getNotes(selectedDate, chatId ?? null) can race if the user changes selectedDate quickly—slower responses for an older date may overwrite newer results.
You can guard with a simple cancellation flag:
useEffect(() => {
- const fetchNotes = async () => {
- const fetchedNotes = await getNotes(selectedDate, chatId ?? null)
- setNotes(fetchedNotes)
- }
- fetchNotes()
+ let cancelled = false
+ const fetchNotes = async () => {
+ const fetchedNotes = await getNotes(selectedDate, chatId ?? null)
+ if (!cancelled) {
+ setNotes(fetchedNotes)
+ }
+ }
+ fetchNotes()
+ return () => {
+ cancelled = true
+ }
}, [selectedDate, chatId])This keeps the notes list in sync with the latest selected date.
📝 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.
| useEffect(() => { | |
| const fetchNotes = async () => { | |
| const fetchedNotes = await getNotes(selectedDate, chatId ?? null) | |
| setNotes(fetchedNotes) | |
| } | |
| fetchNotes() | |
| }, [selectedDate, chatId]) | |
| useEffect(() => { | |
| let cancelled = false | |
| const fetchNotes = async () => { | |
| const fetchedNotes = await getNotes(selectedDate, chatId ?? null) | |
| if (!cancelled) { | |
| setNotes(fetchedNotes) | |
| } | |
| } | |
| fetchNotes() | |
| return () => { | |
| cancelled = true | |
| } | |
| }, [selectedDate, chatId]) |
🤖 Prompt for AI Agents
In components/calendar-notepad-enhanced.tsx around lines 29 to 35, the async
fetchNotes call can race when selectedDate changes rapidly causing older
responses to overwrite newer ones; add a local cancellation flag (e.g., let
cancelled = false) inside the effect, check it before calling setNotes, and
return a cleanup function that sets cancelled = true so only the latest fetch
updates state; alternatively, if getNotes supports AbortController, pass and
trigger an abort in the cleanup to cancel the in-flight request.
| const handleAddNote = async (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
| if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) { | ||
| if (!noteContent.trim()) return | ||
|
|
||
| // Parse the selected time and combine with selected date | ||
| const [hours, minutes, seconds] = selectedTime.split(':').map(Number) | ||
| const noteDate = new Date(selectedDate) | ||
| noteDate.setHours(hours, minutes, seconds || 0) | ||
|
|
||
| const newNote: NewCalendarNote = { | ||
| date: noteDate, | ||
| content: noteContent, | ||
| chatId: chatId ?? null, | ||
| userId: '', // This will be set on the server | ||
| locationTags: taggedLocation, | ||
| userTags: null, | ||
| mapFeatureId: null, | ||
| timezone: selectedTimezone, | ||
| } | ||
|
|
||
| const savedNote = await saveNote(newNote) | ||
| if (savedNote) { | ||
| setNotes([savedNote, ...notes]) | ||
| setNoteContent("") | ||
| setTaggedLocation(null) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden note creation: guard selectedTime and use functional setNotes
Two small robustness issues in handleAddNote:
selectedTimecan be empty or malformed
If the time input is cleared or never set, selectedTime.split(':').map(Number) can yield NaN values, and noteDate.setHours will produce an invalid Date. Consider guarding:
- // Parse the selected time and combine with selected date
- const [hours, minutes, seconds] = selectedTime.split(':').map(Number)
+ // Parse the selected time and combine with selected date
+ if (!selectedTime) {
+ // Either early-return or set a sensible default (e.g. current time)
+ return
+ }
+ const [hours, minutes, seconds] = selectedTime.split(':').map(Number)- Avoid stale
noteswhen appendingsavedNote
Using setNotes([savedNote, ...notes]) closes over the old notes array and can drop items if multiple saves happen before a re-render. Use the functional updater instead:
- if (savedNote) {
- setNotes([savedNote, ...notes])
+ if (savedNote) {
+ setNotes(prevNotes => [savedNote, ...prevNotes])
setNoteContent("")
setTaggedLocation(null)
}Together these make note creation more predictable.
🤖 Prompt for AI Agents
In components/calendar-notepad-enhanced.tsx around lines 69-96, guard against
empty/malformed selectedTime before parsing and switch to the functional state
updater when adding saved notes: validate that selectedTime is a non-empty
string and split results are numeric (or default hours/minutes/seconds to 0) and
bail out or use a safe default if parsing yields NaN, and replace
setNotes([savedNote, ...notes]) with setNotes(prev => [savedNote, ...prev]) so
appends won’t drop items when multiple saves occur concurrently.
| <button | ||
| onClick={() => setDateOffset(dateOffset - 7)} | ||
| className="p-2 text-muted-foreground hover:text-foreground" | ||
| > | ||
| <ChevronLeft className="h-5 w-5" /> | ||
| </button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Add explicit type="button" to non-form buttons (Biome a11y warning)
Biome is correctly flagging these <button>s without an explicit type. While they’re not inside a <form> today, adding type="button" avoids accidental submit behavior if the markup is ever refactored and silences the linter:
- <button
+ <button
+ type="button"
onClick={() => setDateOffset(dateOffset - 7)}
@@
- <button
+ <button
+ type="button"
onClick={() => setDateOffset(dateOffset + 7)}
@@
- <button
+ <button
+ type="button"
onClick={handleTagLocation}
@@
- <button
+ <button
+ type="button"
onClick={() => handleFlyTo(note.locationTags)} This is a small change that improves robustness and satisfies the a11y lint rule.
Also applies to: 164-168, 209-215, 246-251
🧰 Tools
🪛 Biome (2.1.2)
[error] 136-140: Provide an explicit type prop for the button element.
The default type of a button is submit, which causes the submission of a form when placed inside a form element. This is likely not the behaviour that you want inside a React application.
Allowed button types are: submit, button or reset
(lint/a11y/useButtonType)
🤖 Prompt for AI Agents
In components/calendar-notepad-enhanced.tsx around lines 136-141 (and similarly
at 164-168, 209-215, 246-251), the <button> elements lack an explicit type which
can cause accidental form submits and triggers Biome a11y warnings; update each
of these buttons to include type="button" (e.g., <button type="button" ...>) so
they are explicitly non-submit buttons and the linter warning is silenced.
| const [lng, lat] = note.locationTags.coordinates | ||
| const date = new Date(note.date) | ||
|
|
||
| // Format time as HH:MM:SS | ||
| const hours = String(date.getHours()).padStart(2, '0') | ||
| const minutes = String(date.getMinutes()).padStart(2, '0') | ||
| const seconds = String(date.getSeconds()).padStart(2, '0') | ||
|
|
||
| // Format date as DD/MM/YYYY | ||
| const day = String(date.getDate()).padStart(2, '0') | ||
| const month = String(date.getMonth() + 1).padStart(2, '0') | ||
| const year = date.getFullYear() | ||
|
|
||
| const timezone = note.timezone || 'UTC' | ||
|
|
||
| return `${lat.toFixed(6)}, ${lng.toFixed(6)} TIME (${hours}:${minutes}:${seconds}): ${day}/${month}/${year} ${timezone}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Date formatting ignores the note's timezone.
Lines 23-30 use Date.getHours(), getMinutes(), getSeconds(), etc., which return values in the browser's local timezone, not the note.timezone field. This breaks timezone-aware formatting—if a note is tagged with America/New_York but the user views it from Asia/Tokyo, the formatted time will incorrectly reflect Tokyo time.
Use Intl.DateTimeFormat with the timeZone option to format in the note's timezone:
- // Format time as HH:MM:SS
- const hours = String(date.getHours()).padStart(2, '0')
- const minutes = String(date.getMinutes()).padStart(2, '0')
- const seconds = String(date.getSeconds()).padStart(2, '0')
-
- // Format date as DD/MM/YYYY
- const day = String(date.getDate()).padStart(2, '0')
- const month = String(date.getMonth() + 1).padStart(2, '0')
- const year = date.getFullYear()
-
const timezone = note.timezone || 'UTC'
+
+ // Format date and time in the note's timezone
+ const formatter = new Intl.DateTimeFormat('en-GB', {
+ timeZone: timezone,
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ hour12: false,
+ })
+
+ const parts = formatter.formatToParts(date)
+ const get = (type: string) => parts.find(p => p.type === type)?.value || ''
+
+ const hours = get('hour')
+ const minutes = get('minute')
+ const seconds = get('second')
+ const day = get('day')
+ const month = get('month')
+ const year = get('year')
return `${lat.toFixed(6)}, ${lng.toFixed(6)} TIME (${hours}:${minutes}:${seconds}): ${day}/${month}/${year} ${timezone}`📝 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.
| const [lng, lat] = note.locationTags.coordinates | |
| const date = new Date(note.date) | |
| // Format time as HH:MM:SS | |
| const hours = String(date.getHours()).padStart(2, '0') | |
| const minutes = String(date.getMinutes()).padStart(2, '0') | |
| const seconds = String(date.getSeconds()).padStart(2, '0') | |
| // Format date as DD/MM/YYYY | |
| const day = String(date.getDate()).padStart(2, '0') | |
| const month = String(date.getMonth() + 1).padStart(2, '0') | |
| const year = date.getFullYear() | |
| const timezone = note.timezone || 'UTC' | |
| return `${lat.toFixed(6)}, ${lng.toFixed(6)} TIME (${hours}:${minutes}:${seconds}): ${day}/${month}/${year} ${timezone}` | |
| const [lng, lat] = note.locationTags.coordinates | |
| const date = new Date(note.date) | |
| const timezone = note.timezone || 'UTC' | |
| // Format date and time in the note's timezone | |
| const formatter = new Intl.DateTimeFormat('en-GB', { | |
| timeZone: timezone, | |
| year: 'numeric', | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit', | |
| hour12: false, | |
| }) | |
| const parts = formatter.formatToParts(date) | |
| const get = (type: string) => parts.find(p => p.type === type)?.value || '' | |
| const hours = get('hour') | |
| const minutes = get('minute') | |
| const seconds = get('second') | |
| const day = get('day') | |
| const month = get('month') | |
| const year = get('year') | |
| return `${lat.toFixed(6)}, ${lng.toFixed(6)} TIME (${hours}:${minutes}:${seconds}): ${day}/${month}/${year} ${timezone}` |
🤖 Prompt for AI Agents
In lib/utils/calendar-context.ts around lines 19 to 34, the code formats
date/time using Date.getHours()/getMinutes()/getSeconds()/getDate()/getMonth()
which use the runtime's local timezone instead of the note.timezone; replace the
manual getters with Intl.DateTimeFormat (or
DateTimeFormat.prototype.formatToParts) configured with timeZone: note.timezone
|| 'UTC' to generate the hour, minute, second, day, month and year parts, then
assemble the formatted string using those parts and keep the existing
latitude/longitude output and timezone label.
| return '' | ||
| } | ||
|
|
||
| const [lng, lat] = note.locationTags.coordinates |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add validation for coordinates array length.
Line 19 destructures coordinates without verifying it has at least 2 elements. If locationTags.coordinates is an empty array or has only 1 element, this will silently assign undefined and cause toFixed(6) to throw at Line 34.
const [lng, lat] = note.locationTags.coordinates
+ if (lng === undefined || lat === undefined || typeof lng !== 'number' || typeof lat !== 'number') {
+ return ''
+ }📝 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.
| const [lng, lat] = note.locationTags.coordinates | |
| const [lng, lat] = note.locationTags.coordinates | |
| if (lng === undefined || lat === undefined || typeof lng !== 'number' || typeof lat !== 'number') { | |
| return '' | |
| } |
🤖 Prompt for AI Agents
In lib/utils/calendar-context.ts around line 19, the code destructures
note.locationTags.coordinates without validating its length, which can produce
undefined values and cause toFixed(6) to throw; update the code to first verify
that note.locationTags && Array.isArray(note.locationTags.coordinates) &&
note.locationTags.coordinates.length >= 2, confirm both entries are numbers (or
coerce/parse to numbers), then safely assign lng/lat from the validated array
(or bail out/return a sensible default or throw a clear error) before calling
toFixed(6); ensure subsequent code handles the validation failure path.
| export function parsePlaceTimeContext(contextString: string): { | ||
| latitude: number | ||
| longitude: number | ||
| time: string | ||
| date: string | ||
| timezone: string | ||
| } | null { | ||
| // Match pattern: lat, lng TIME (HH:MM:SS): DD/MM/YYYY Timezone | ||
| const pattern = /^([-\d.]+),\s*([-\d.]+)\s+TIME\s+\((\d{2}:\d{2}:\d{2})\):\s+(\d{2}\/\d{2}\/\d{4})\s+(.+)$/ | ||
| const match = contextString.match(pattern) | ||
|
|
||
| if (!match) { | ||
| return null | ||
| } | ||
|
|
||
| return { | ||
| latitude: parseFloat(match[1]), | ||
| longitude: parseFloat(match[2]), | ||
| time: match[3], | ||
| date: match[4], | ||
| timezone: match[5] | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding validation for parsed values.
The function parses coordinates and date/time components but doesn't validate them. Invalid values (e.g., latitude 200, date 32/13/2024) will pass through silently.
return {
latitude: parseFloat(match[1]),
longitude: parseFloat(match[2]),
time: match[3],
date: match[4],
timezone: match[5]
}
+
+ const lat = parseFloat(match[1])
+ const lng = parseFloat(match[2])
+
+ // Validate coordinate ranges
+ if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
+ return null
+ }
+
+ return {
+ latitude: lat,
+ longitude: lng,
+ time: match[3],
+ date: match[4],
+ timezone: match[5]
+ }📝 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.
| export function parsePlaceTimeContext(contextString: string): { | |
| latitude: number | |
| longitude: number | |
| time: string | |
| date: string | |
| timezone: string | |
| } | null { | |
| // Match pattern: lat, lng TIME (HH:MM:SS): DD/MM/YYYY Timezone | |
| const pattern = /^([-\d.]+),\s*([-\d.]+)\s+TIME\s+\((\d{2}:\d{2}:\d{2})\):\s+(\d{2}\/\d{2}\/\d{4})\s+(.+)$/ | |
| const match = contextString.match(pattern) | |
| if (!match) { | |
| return null | |
| } | |
| return { | |
| latitude: parseFloat(match[1]), | |
| longitude: parseFloat(match[2]), | |
| time: match[3], | |
| date: match[4], | |
| timezone: match[5] | |
| } | |
| } | |
| export function parsePlaceTimeContext(contextString: string): { | |
| latitude: number | |
| longitude: number | |
| time: string | |
| date: string | |
| timezone: string | |
| } | null { | |
| // Match pattern: lat, lng TIME (HH:MM:SS): DD/MM/YYYY Timezone | |
| const pattern = /^([-\d.]+),\s*([-\d.]+)\s+TIME\s+\((\d{2}:\d{2}:\d{2})\):\s+(\d{2}\/\d{2}\/\d{4})\s+(.+)$/ | |
| const match = contextString.match(pattern) | |
| if (!match) { | |
| return null | |
| } | |
| const lat = parseFloat(match[1]) | |
| const lng = parseFloat(match[2]) | |
| // Validate coordinate ranges | |
| if (lat < -90 || lat > 90 || lng < -180 || lng > 180) { | |
| return null | |
| } | |
| return { | |
| latitude: lat, | |
| longitude: lng, | |
| time: match[3], | |
| date: match[4], | |
| timezone: match[5] | |
| } | |
| } |
🤖 Prompt for AI Agents
In lib/utils/calendar-context.ts around lines 43 to 65, the parser returns
parsed numbers/strings without validating them; add validation after parsing so
invalid coordinates, times or dates are rejected. Specifically, verify latitude
is between -90 and 90 and longitude between -180 and 180, validate the time
components (HH 00–23, MM/SS 00–59) parsed from the TIME group, and validate the
DD/MM/YYYY date is a real calendar date (e.g., construct and compare a Date or
use a reliable date parser to ensure day/month/year boundaries and leap years).
If any validation fails, return null (or propagate a clear error) instead of
returning malformed values; keep the original return shape when values are
valid.
- Fix import: use useMCPMapClient instead of useMapboxMCP - Handle LngLatLike type properly for targetPosition - Fix geocoding result access path (result.location.place_name) - Add timezone field to original CalendarNotepad for backward compatibility Build now passes successfully.
User description
Summary
This PR implements an enhanced calendar UI that integrates with Mapbox MCP to provide comprehensive place and time context management.
Features
1. Enhanced Time Management
2. Location Tagging with Mapbox MCP
3. Structured Context Format
All location-tagged notes display context in the standardized format:
Example:
40.758896, -73.985130 TIME (14:30:45): 20/11/2025 America/New_YorkChanges
timezonefield tocalendar_notestableCalendarNotepadEnhancedwith time/timezone selectorscalendar-context.tswith formatting functionsCALENDAR_ENHANCEMENT.mdFiles Changed
lib/db/schema.ts: Added timezone fieldlib/types/index.ts: Updated CalendarNote typecomponents/calendar-notepad-enhanced.tsx: New enhanced componentcomponents/chat.tsx: Switched to enhanced calendarlib/utils/calendar-context.ts: New utility functionsdrizzle/migrations/0002_add_timezone_to_calendar_notes.sql: MigrationCALENDAR_ENHANCEMENT.md: DocumentationTesting
The implementation has been verified for:
Migration
Run
bun run db:migrateto apply database changes.Related Issues
Addresses the calendar place/time context feature request.
PR Type
Enhancement
Description
Add timezone field to calendar_notes table for temporal context
Create CalendarNotepadEnhanced component with time picker and timezone selector
Implement standardized place/time context format with coordinates, time, date, timezone
Add calendar-context utility functions for formatting and parsing location/time data
Integrate Mapbox MCP geocoding for automatic place name resolution
Support 20+ common timezones with auto-detection of user's current timezone
Update chat component to use enhanced calendar notepad
Add comprehensive documentation for calendar enhancement features
Diagram Walkthrough
File Walkthrough
schema.ts
Add timezone field to calendar schemalib/db/schema.ts
timezonefield tocalendarNotestable as optional varchar(100)index.ts
Add timezone property to CalendarNote typelib/types/index.ts
timezone: string | nullproperty to CalendarNote typecalendar-context.ts
Add calendar context formatting and parsing utilitieslib/utils/calendar-context.ts
formatPlaceTimeContext()to format notes as "lat, lng TIME(HH:MM:SS): DD/MM/YYYY Timezone"
parsePlaceTimeContext()to parse context strings back intocomponents
createGeoJSONPoint()for GeoJSON Point creationgetCurrentTimezone()to detect user's browser timezoneCOMMON_TIMEZONESarray with 20+ supported timezonescalendar-notepad-enhanced.tsx
Implement enhanced calendar notepad with time/locationcomponents/calendar-notepad-enhanced.tsx
format)
chat.tsx
Switch chat to use enhanced calendar componentcomponents/chat.tsx
CalendarNotepadimport withCalendarNotepadEnhancedcomponent
0002_add_timezone_to_calendar_notes.sql
Add database migration for timezone fielddrizzle/migrations/0002_add_timezone_to_calendar_notes.sql
timezoneVARCHAR(100) column in calendar_notestable
CALENDAR_ENHANCEMENT.md
Add comprehensive calendar enhancement documentationCALENDAR_ENHANCEMENT.md
support
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.