diff --git a/packages/notion-types/src/collection-view.ts b/packages/notion-types/src/collection-view.ts index 712a8189d..97da19807 100644 --- a/packages/notion-types/src/collection-view.ts +++ b/packages/notion-types/src/collection-view.ts @@ -119,8 +119,8 @@ export interface BoardCollectionView extends BaseCollectionView { export interface CalendarCollectionView extends BaseCollectionView { type: 'calendar' - - // TODO + visible: boolean + property?: PropertyID } export type CollectionView = diff --git a/packages/notion-utils/src/get-page-property-from-id.ts b/packages/notion-utils/src/get-page-property-from-id.ts new file mode 100644 index 000000000..581c8d7c0 --- /dev/null +++ b/packages/notion-utils/src/get-page-property-from-id.ts @@ -0,0 +1,89 @@ +import { Block, DateFormat, ExtendedRecordMap } from 'notion-types' + +import { getTextContent } from './get-text-content' + +/** + * Gets the value of a collection property for a given page (collection item). + * + * @param propertyId property id + * @param block Page block, often be first block in blockMap + * @param recordMap + * @returns - The return value types will follow the following principles: + * 1. if property is date type, it will return `number` or `number[]`(depends on `End Date` switch) + * 2. property is text-like will return `string` + * 3. multi select property will return `string[]` + * 4. checkbox property return `boolean` + * @todo complete all no-text property type + */ +export function getPagePropertyFromId< + T = string | number | boolean | string[] | number[] +>(propertyId: string, block: Block, recordMap: ExtendedRecordMap): T +export function getPagePropertyFromId( + propertyId: string, + block: Block, + recordMap: ExtendedRecordMap +) { + try { + if (!block.properties || !Object.keys(recordMap.collection)) { + // console.warn( + // `block ${block.id} has no properties or this recordMap has no collection record` + // ) + return null + } + + const collection = recordMap.collection[block.parent_id]?.value + + if (collection) { + if (!propertyId) { + return null + } + + const { type } = collection.schema[propertyId] + const content = getTextContent(block.properties[propertyId]) + + switch (type) { + case 'created_time': + return block.created_time + + case 'multi_select': + return content.split(',') + + case 'date': { + const property = block.properties[propertyId] as [['‣', [DateFormat]]] + const formatDate = property[0][1][0][1] + + if (formatDate.type == 'datetime') { + return new Date( + `${formatDate.start_date} ${formatDate.start_time}` + ).getTime() + } else if (formatDate.type == 'date') { + return new Date(formatDate.start_date).getTime() + } else if (formatDate.type == 'datetimerange') { + const { start_date, start_time, end_date, end_time } = formatDate + const startTime = new Date(`${start_date} ${start_time}`).getTime() + const endTime = new Date(`${end_date} ${end_time}`).getTime() + return [startTime, endTime] + } else { + const startTime = new Date(formatDate.start_date).getTime() + const endTime = new Date(formatDate.end_date).getTime() + return [startTime, endTime] + } + } + + case 'checkbox': + return content == 'Yes' + + case 'last_edited_time': + return block.last_edited_time + + default: + return content + } + } + } catch { + // ensure that no matter what, we don't throw errors because of an unexpected + // collection data format + } + + return null +} diff --git a/packages/notion-utils/src/index.ts b/packages/notion-utils/src/index.ts index e86921b0e..433dca881 100644 --- a/packages/notion-utils/src/index.ts +++ b/packages/notion-utils/src/index.ts @@ -4,6 +4,7 @@ export * from './get-block-icon' export * from './get-block-collection-id' export * from './get-page-title' export * from './get-page-property' +export * from './get-page-property-from-id' export * from './get-date-value' export * from './get-block-parent-page' export * from './get-page-table-of-contents' diff --git a/packages/react-notion-x/src/context.tsx b/packages/react-notion-x/src/context.tsx index 63140538d..e5d17f44b 100644 --- a/packages/react-notion-x/src/context.tsx +++ b/packages/react-notion-x/src/context.tsx @@ -32,6 +32,8 @@ export interface NotionContext { previewImages: boolean forceCustomImages: boolean showCollectionViewDropdown: boolean + showCalendarControls: boolean + startWeekOnMonday: boolean showTableOfContents: boolean minTableOfContentsItems: number linkTableTitleProperties: boolean @@ -66,6 +68,8 @@ export interface PartialNotionContext { isLinkCollectionToUrlProperty?: boolean showTableOfContents?: boolean + showCalendarControls?: boolean + startWeekOnMonday?: boolean minTableOfContentsItems?: number defaultPageIcon?: string @@ -167,6 +171,8 @@ const defaultNotionContext: NotionContext = { isLinkCollectionToUrlProperty: false, showTableOfContents: false, + showCalendarControls: true, + startWeekOnMonday: false, minTableOfContentsItems: 3, defaultPageIcon: null, diff --git a/packages/react-notion-x/src/icons/left-chevron.tsx b/packages/react-notion-x/src/icons/left-chevron.tsx new file mode 100644 index 000000000..b2e5e0ccd --- /dev/null +++ b/packages/react-notion-x/src/icons/left-chevron.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +function SvgLeftChevron(props: React.SVGProps) { + return ( + + + + ) +} + +export default SvgLeftChevron diff --git a/packages/react-notion-x/src/icons/right-chevron.tsx b/packages/react-notion-x/src/icons/right-chevron.tsx new file mode 100644 index 000000000..10862f272 --- /dev/null +++ b/packages/react-notion-x/src/icons/right-chevron.tsx @@ -0,0 +1,11 @@ +import * as React from 'react' + +function SvgRightChevron(props: React.SVGProps) { + return ( + + + + ) +} + +export default SvgRightChevron diff --git a/packages/react-notion-x/src/renderer.tsx b/packages/react-notion-x/src/renderer.tsx index 4f063fba0..d967dca69 100644 --- a/packages/react-notion-x/src/renderer.tsx +++ b/packages/react-notion-x/src/renderer.tsx @@ -38,6 +38,8 @@ export const NotionRenderer: React.FC<{ isImageZoomable?: boolean showTableOfContents?: boolean + showCalendarControls?: boolean + startWeekOnMonday?: boolean minTableOfContentsItems?: number defaultPageIcon?: string @@ -77,6 +79,8 @@ export const NotionRenderer: React.FC<{ isLinkCollectionToUrlProperty, isImageZoomable = true, showTableOfContents, + showCalendarControls, + startWeekOnMonday, minTableOfContentsItems, defaultPageIcon, defaultPageCover, @@ -113,6 +117,8 @@ export const NotionRenderer: React.FC<{ linkTableTitleProperties={linkTableTitleProperties} isLinkCollectionToUrlProperty={isLinkCollectionToUrlProperty} showTableOfContents={showTableOfContents} + showCalendarControls={showCalendarControls} + startWeekOnMonday={startWeekOnMonday} minTableOfContentsItems={minTableOfContentsItems} defaultPageIcon={defaultPageIcon} defaultPageCover={defaultPageCover} diff --git a/packages/react-notion-x/src/styles.css b/packages/react-notion-x/src/styles.css index 1ea9a0c96..9dbb9bf6c 100644 --- a/packages/react-notion-x/src/styles.css +++ b/packages/react-notion-x/src/styles.css @@ -16,6 +16,9 @@ --bg-color-0: rgba(135, 131, 120, 0.15); --bg-color-1: rgb(247, 246, 243); --bg-color-2: rgba(135, 131, 120, 0.15); + --bg-color-3: #fff; + + --box-shadow: rgba(15, 15, 15, 0.1); --select-color-0: rgb(46, 170, 220); --select-color-1: rgba(45, 170, 219, 0.3); @@ -103,6 +106,9 @@ --bg-color-0: rgb(71, 76, 80); --bg-color-1: rgb(63, 68, 71); --bg-color-2: rgba(135, 131, 120, 0.15); + --bg-color-3: rgb(47, 47, 47); + + --box-shadow: rgba(20, 20, 20, 0.1); --notion-red: rgb(255, 115, 105); --notion-pink: rgb(226, 85, 161); @@ -1533,7 +1539,7 @@ svg.notion-page-icon { overflow: hidden; text-decoration: none; - box-shadow: rgba(15, 15, 15, 0.1) 0 0 0 1px, rgba(15, 15, 15, 0.1) 0 2px 4px; + box-shadow: var(--box-shadow) 0 0 0 1px, var(--box-shadow) 0 2px 4px; border-radius: 3px; background: var(--bg-color); color: var(--fg-color); @@ -1674,6 +1680,259 @@ svg.notion-page-icon { text-overflow: ellipsis; } +.notion-calendar-view { + position: relative; + padding-left: 1px; +} + +.notion-calendar-header { + position: absolute; + left: 0; + right: 0; + background: var(--bg-color); + z-index: 83; +} + +.notion-calendar-header-inner { + display: flex; + height: 42px; + align-items: center; +} + +.notion-calendar-header-inner-date { + font-weight: 600; + margin-left: 8px; + margin-right: 8px; + line-height: 1; + font-size: 14px; +} + +.notion-calendar-header-inner-controls-prev { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border-radius: 3px; + height: 24px; + width: 24px; + padding: 0px; +} + +.notion-calendar-header-inner-controls-prev:hover, +.notion-calendar-header-inner-controls-today:hover, +.notion-calendar-header-inner-controls-next:hover { + background: var(--notion-gray_background); +} + +.notion-calendar-header-inner-controls-today { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + display: inline-flex; + align-items: center; + flex-shrink: 0; + white-space: nowrap; + height: 24px; + border-radius: 3px; + font-size: 14px; + line-height: 1.2; + min-width: 0px; + padding-left: 6px; + padding-right: 6px; + color: var(--fg-color); +} + +.notion-calendar-header-inner-controls-next { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border-radius: 3px; + height: 24px; + width: 24px; + padding: 0px; +} + +.notion-calendar-header-inner-controls-next-svg, +.notion-calendar-header-inner-controls-prev-svg { + width: 14px; + height: 14px; + display: block; + fill: var(--fg-color-2); + flex-shrink: 0; + backface-visibility: hidden; +} + +.notion-calendar-header-days { + display: flex; + margin-top: 0px; + box-shadow: var(--notion-gray_background) 0px 1px 0px; +} + +.notion-calendar-header-days-day { + flex-grow: 1; + flex-basis: 0px; + text-align: center; + font-size: 12px; + height: 24px; + color: var(--fg-color-3); +} + +.notion-calendar-body { + box-shadow: var(--notion-gray_background) -1px 0px 0px; + margin-top: 1px; + overflow: hidden; +} + +.notion-calendar-body-inner { + position: relative; + display: flex; +} + +.notion-calendar-body-inner-week { + position: relative; + flex: 1 0 0px; + border-right: 1px solid var(--notion-gray_background); + border-bottom: 1px solid var(--notion-gray_background); + cursor: default; +} + +.notion-calendar-body-inner-week-dif { + background: var(--bg-color-1); +} + +.notion-calendar-body-inner-day { + position: absolute; + font-size: 14px; + top: 4px; + right: 10px; + height: 24px; + line-height: 24px; +} + +.notion-calendar-body-inner-day-today { + width: 24px; + border-radius: 100%; + text-align: center; + color: white; + background: var(--notion-red); +} + +.notion-calendar-body-inner-day-this-month { + color: var(--fg-color-4); + text-align: right; + transition: color 100ms ease-out 0s; +} + +.notion-calendar-body-inner-day-other-month { + color: var(--fg-color-2); + text-align: right; + transition: color 100ms ease-out 0s; +} + +.notion-calendar-body-inner-card { + width: calc(14.2857%); + position: absolute; + padding: 3px 6px; + top: 30px; + z-index: 2; +} + +.notion-calendar-body-inner-card-inner { + display: block; + color: inherit; + text-decoration: none; + height: 100%; + background: var(--bg-color-3); + border-radius: 3px; + box-shadow: var(--box-shadow) 0px 0px 0px 1px, var(--box-shadow) 0px 2px 4px; +} + +.notion-calendar-body-inner-card-inner-box { + user-select: none; + transition: background 20ms ease-in 0s; + cursor: pointer; + width: 100%; + display: flex; + position: relative; + padding-top: 2px; + padding-bottom: 2px; + height: 100%; + align-items: flex-start; + flex-direction: column; +} + +.notion-calendar-body-inner-card-inner:hover { + background: var(--notion-gray_background); +} + +.notion-calendar-body-inner-card-inner-box-title { + padding-left: 6px; + padding-right: 6px; + overflow: hidden; + width: 100%; + font-size: 12px; +} + +.notion-calendar-body-inner-card-inner-box-title-inner { + display: flex; + align-items: center; + height: 20px; +} + +.notion-calendar-body-inner-card-inner-box-title-inner-icon { + user-select: none; + transition: background 20ms ease-in 0s; + display: flex; + align-items: center; + justify-content: center; + height: 12px; + width: 12px; + border-radius: 0.25em; + flex-shrink: 0; + margin-right: 4px; + margin-top: 2px; +} + +.notion-calendar-body-inner-card-inner-box-title-inner-icon-svg { + width: 10.8px; + height: 10.8px; + display: block; + fill: var(--fg-color-3); + flex-shrink: 0; + backface-visibility: hidden; +} + +.notion-calendar-body-inner-card-inner-box-title-inner-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex-grow: 1; + font-size: 12px; + font-weight: 600; +} + +.notion-calendar-body-inner-card-inner-box-properties { + padding-left: 6px; + padding-right: 6px; + overflow: hidden; + width: 100%; +} + +.notion-calendar-body-inner-card-inner-box-properties-property { + display: flex; + align-items: center; + font-size: 12px; + height: 20px; + white-space: nowrap; +} + .notion-board { width: 100vw; max-width: 100vw; @@ -2447,7 +2706,7 @@ svg.notion-page-icon { .notion-search { box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, - rgba(15, 15, 15, 0.1) 0px 5px 10px, rgba(15, 15, 15, 0.2) 0px 15px 40px; + var(--box-shadow) 0px 5px 10px, rgba(15, 15, 15, 0.2) 0px 15px 40px; border-radius: 3px; background: var(--bg-color); @@ -2628,9 +2887,6 @@ svg.notion-page-icon { margin-bottom: 1em; } -.notion-collection-group > summary { -} - .notion-collection-group > summary > div { transform: scale(0.85); transform-origin: 0% 50%; @@ -2890,7 +3146,7 @@ padding-left: 4px; display: flex; flex-direction: row; flex-wrap: wrap; - width: 120% + width: 120%; } .notion-collection-view-tabs-content-item { @@ -2940,9 +3196,9 @@ padding-left: 4px; } .nested-form-link { - background: none!important; + background: none !important; border: none; - padding: 0!important; + padding: 0 !important; text-decoration: underline; cursor: pointer; } diff --git a/packages/react-notion-x/src/third-party/collection-view-calendar.tsx b/packages/react-notion-x/src/third-party/collection-view-calendar.tsx new file mode 100644 index 000000000..26617e3a7 --- /dev/null +++ b/packages/react-notion-x/src/third-party/collection-view-calendar.tsx @@ -0,0 +1,470 @@ +import * as React from 'react' + +import { CalendarCollectionView, PageBlock } from 'notion-types' +import { getBlockIcon, getPagePropertyFromId } from 'notion-utils' + +import { PageIcon } from '../components/page-icon' +import { useNotionContext } from '../context' +import SvgLeftChevron from '../icons/left-chevron' +import SvgRightChevron from '../icons/right-chevron' +import { CollectionViewProps } from '../types' +import { cs, getWeeksInMonth } from '../utils' +import { CollectionGroup } from './collection-group' +import { getCollectionGroups } from './collection-utils' +import { Property } from './property' + +const defaultBlockIds: string[] = [] +const currentYear = new Date() +const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' +] + +const monthsShort = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' +] + +export const CollectionViewCalendar: React.FC = ({ + collection, + collectionView, + collectionData +}) => { + const isGroupedCollection = collectionView?.format?.collection_group_by + + if (isGroupedCollection) { + const collectionGroups = getCollectionGroups( + collection, + collectionView, + collectionData + ) + + return collectionGroups.map((group, index) => ( + + )) + } + + const blockIds = + (collectionData['collection_group_results']?.blockIds ?? + collectionData['results:relation:uncategorized']?.blockIds ?? + collectionData.blockIds) || + defaultBlockIds + + return ( + + ) +} + +function Calendar({ blockIds, collectionView, collection }) { + const { + showCalendarControls, + startWeekOnMonday, + components, + recordMap, + mapPageUrl + } = useNotionContext() + + const [currentMonth, setCurrentMonth] = React.useState(0) + const [weeksArr, setWeeksArr] = React.useState( + getWeeksInMonth( + currentYear.getFullYear(), + currentYear.getMonth(), + startWeekOnMonday + ) + ) + + /** + * Set next month to be shown + */ + const nextMonth = () => { + if (currentYear.getMonth() == 11) { + currentYear.setFullYear(currentYear.getFullYear() + 1) + currentYear.setMonth(0) + } else { + currentYear.setMonth(currentYear.getMonth() + 1) + } + + setCurrentMonth(0) + + setWeeksArr( + getWeeksInMonth( + currentYear.getFullYear(), + currentYear.getMonth(), + startWeekOnMonday + ) + ) + } + /** + * Set previous month to be shown + */ + const prevMonth = () => { + if (currentYear.getMonth() == 0) { + currentYear.setFullYear(currentYear.getFullYear() - 1) + currentYear.setMonth(11) + } else { + currentYear.setMonth(currentYear.getMonth() - 1) + } + + setCurrentMonth(0) + + setWeeksArr( + getWeeksInMonth( + currentYear.getFullYear(), + currentYear.getMonth(), + startWeekOnMonday + ) + ) + } + /** + * Set current (in the user time) month to be shwon + */ + const nowMonth = () => { + currentYear.setMonth(new Date().getMonth()) + currentYear.setFullYear(new Date().getFullYear()) + + setCurrentMonth(0) + + setWeeksArr( + getWeeksInMonth( + currentYear.getFullYear(), + currentYear.getMonth(), + startWeekOnMonday + ) + ) + } + + /** + * Get the highest numer of pages for a specific weeknumer in the current month + * @param weekNumber + * @returns + */ + const checkWeek = (weekNumber: number) => { + let max = 0 + + for (let i = 0; i < 7; i++) { + const newMax = getPagesThisDay(weeksArr[weekNumber][i]).length + if (max < newMax) max = newMax + } + + return max + } + + /** + * Get all the pages that has a block in the day parameter + * @param day + * @returns The blocks in a day + */ + const getPagesThisDay = (day: { date: number; month: number }) => { + const daysTo: PageBlock[] = [] + + blockIds?.map((blockId: string) => { + const block = recordMap.block[blockId]?.value as PageBlock + if (!block) return null + + // Get date from calendar view query + const blockDate = getPagePropertyFromId( + collectionView.query2.calendar_by, + block, + recordMap + ) + const blockDateDATE = new Date(blockDate as number) + if ( + blockDateDATE.getDate() == day.date && + blockDateDATE.getMonth() == day.month && + blockDateDATE.getFullYear() == currentYear.getFullYear() + ) { + daysTo.push(block) + } + }) + + return daysTo + } + + return ( +
+
+
+
+ {months[currentYear.getMonth()] + ' ' + currentYear.getFullYear()} +
+ +
+ + {/* Calendar controls | prev - today - next */} + {showCalendarControls && ( + <> +
+ +
+ +
+ Today +
+
+ +
+ + )} +
+ +
+ {startWeekOnMonday ? ( + <> +
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+
Sun
+ + ) : ( + <> +
Sun
+
Mon
+
Tue
+
Wed
+
Thu
+
Fri
+
Sat
+ + )} +
+
+ +
+ +
+ {/* Rows */} + {weeksArr.map((i, indexI) => ( +
+ {/* Columns */} + {i.map((day, indexY) => ( + <> + {/* Print the days blocks with the number and month if is day 1 */} +
+
= 28) && + currentYear.getMonth() == day.month) + ? 'notion-calendar-body-inner-day-this-month' + : 'notion-calendar-body-inner-day-other-month' + )} + > + {day.date == 1 && + !( + day.date == new Date().getDate() && + day.month == new Date().getMonth() && + day.year == new Date().getFullYear() + ) + ? `${monthsShort[day.month + currentMonth]} ${day.date}` + : day.date} +
+
+ + {/* Print the blocks the day */} + {getPagesThisDay(day).map((block, sum) => { + // Get date from calendar view query + const blockDate = getPagePropertyFromId( + collectionView.query2.calendar_by, + block, + recordMap + ) + const blockDateDATE = new Date(blockDate as number) + const dayBlock = startWeekOnMonday + ? blockDateDATE.getDay() === 0 + ? 6 + : blockDateDATE.getDay() - 1 + : blockDateDATE.getDay() + + const titleSchema = collection.schema.title + const titleData = block?.properties?.title + + if ( + blockDateDATE.getDate() == day.date && + blockDateDATE.getMonth() == day.month && + blockDateDATE.getFullYear() == currentYear.getFullYear() + ) { + return ( +
+ +
+
+
+ {getBlockIcon(block, recordMap) && ( +
+ +
+ )} +
+ +
+
+
+
+ {collectionView.format?.calendar_properties + ?.filter( + (p: CalendarCollectionView) => p.visible + ) + .map((p: CalendarCollectionView, z) => { + const schema = collection.schema[p.property] + const data = + block && block.properties?.[p.property] + + if (!schema) { + return null + } + + return ( +
+ +
+ ) + })} +
+
+
+
+ ) + } + + return null + })} + + ))} +
+ ))} +
+
+ ) +} diff --git a/packages/react-notion-x/src/third-party/collection-view.tsx b/packages/react-notion-x/src/third-party/collection-view.tsx index 53c7185fb..c38adbcb8 100644 --- a/packages/react-notion-x/src/third-party/collection-view.tsx +++ b/packages/react-notion-x/src/third-party/collection-view.tsx @@ -2,6 +2,7 @@ import * as React from 'react' import { CollectionViewProps } from '../types' import { CollectionViewBoard } from './collection-view-board' +import { CollectionViewCalendar } from './collection-view-calendar' import { CollectionViewGallery } from './collection-view-gallery' import { CollectionViewList } from './collection-view-list' import { CollectionViewTable } from './collection-view-table' @@ -22,6 +23,9 @@ export const CollectionViewImpl: React.FC = (props) => { case 'board': return + case 'calendar': + return + default: console.warn('unsupported collection view', collectionView) return null diff --git a/packages/react-notion-x/src/utils.ts b/packages/react-notion-x/src/utils.ts index 5c07bb592..bd034b89e 100644 --- a/packages/react-notion-x/src/utils.ts +++ b/packages/react-notion-x/src/utils.ts @@ -82,3 +82,85 @@ export const getYoutubeId = (url: string): string | null => { return null } + +/** + * Get dates and month in an array of weeks based on the year and on the month + * @param year + * @param month + * @param startWeekOnMonday + * @returns An array of objects with month and date number + */ +export const getWeeksInMonth = ( + year: number, + month: number, + startWeekOnMonday?: boolean +) => { + const weeks = [], + firstDate = new Date(year, month, 1), + lastDate = new Date(year, month + 1, 0), + numDays = lastDate.getDate() + + let start = 1 + let end = -1 + + if (firstDate.getDay() === 1) { + end = 7 + } else if (firstDate.getDay() === 0) { + const preMonthEndDay = new Date(year, month, 0) + + start = preMonthEndDay.getDate() - 6 + 1 + end = 1 + } else { + const preMonthEndDay = new Date(year, month, 0) + + start = + preMonthEndDay.getDate() + + 1 - + firstDate.getDay() + + (startWeekOnMonday ? 1 : 0) + end = 7 - firstDate.getDay() + (startWeekOnMonday ? 1 : 0) + + weeks.push({ + start: start, + end: end + }) + + start = end + 1 + end = end + 7 + } + + while (start <= numDays) { + weeks.push({ + start: start, + end: end + }) + + start = end + 1 + end += 7 + end = start === 1 && end === 8 ? 1 : end + + if (end > numDays && start <= numDays) { + end = end - numDays + + weeks.push({ + start: start, + end: end + }) + + break + } + } + + return weeks.map(({ start, end }, index) => { + const sub = +(start > end && index === 0) + return Array.from({ length: 7 }, (_, index) => { + const date = new Date(year, month - sub, start + index) + + return { + date: date.getDate(), + month: date.getMonth(), + year: date.getFullYear() + } + }) + }) +} diff --git a/readme.md b/readme.md index 476c7748d..e589c4528 100644 --- a/readme.md +++ b/readme.md @@ -202,8 +202,8 @@ For a production example, check out the [Next.js Notion Starter Kit](https://git The majority of Notion blocks and collection views are fully supported. -| Block Type | Supported | Block Type Enum | Notes | -| ------------------------ | ---------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | +| Block Type | Supported | Block Type Enum | Notes | +| ------------------------ | --------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | | Page | ✅ Yes | `page` | | Text | ✅ Yes | `text` | Supports all known text formatting options | | Bookmark | ✅ Yes | `bookmark` | Embedded preview of external URL | @@ -243,7 +243,7 @@ The majority of Notion blocks and collection views are fully supported. | Collection View Gallery | ✅ Yes | `collection_view` | `type = "gallery"` (grid view) | | Collection View Board | ✅ Yes | `collection_view` | `type = "board"` (kanban view) | | Collection View List | ✅ Yes | `collection_view` | `type = "list"` (vertical list view) | -| Collection View Calendar | ❌ Missing | `collection_view` | `type = "calendar"` (embedded calendar view) | +| Collection View Calendar | ✅ Yes | `collection_view` | `type = "calendar"` (embedded calendar view [see more details here](#calendar-view)) | | Collection View Page | ✅ Yes | `collection_view_page` | Collection view as a standalone page | Please let us know if you find any issues or missing blocks. @@ -290,6 +290,20 @@ export default ({ recordMap }) => ( This wraps these next.js components in a compatability layer so `NotionRenderer` can use them the same as their non-next.js equivalents `` and ``. + +## Calendar view +Notion's databases calendar view is one of the most complex views that exists on Notion. +This component is still a Work-In-Progress, **recommended use is for development only**, but will soon be more stable and out of bugs and ready for production deploys. + +At the moment it features: +- Controls (previuous, current, next month views) +- Display block that has ONLY one day duration (without time) +- Show calendar in both SUN-SAT and MON-SUN format (use `startWeekOnMonday` parameter) + +What is unsupported at the moment: +- Display blocks that has a day range +- Display blocks with date and time (it will render but the data and time property will be shown in a bad way) + ## Related - [Next.js Template](https://github.com/transitive-bullshit/nextjs-notion-starter-kit) - The easiest way to deploy a self-hosted Notion site with Next.js and Vercel.