From 4b8ee59cb70f3c207e6a432d5f8146a74fe67e24 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Sat, 8 Nov 2025 12:49:19 -0800 Subject: [PATCH 01/48] Add events table component WIP --- src/components/EventsTable/EventsTable.tsx | 248 ++++++++++++++++++ .../EventsTable/EventsTableWrapper.tsx | 15 ++ src/components/ui/table.tsx | 91 +++++++ 3 files changed, 354 insertions(+) create mode 100644 src/components/EventsTable/EventsTable.tsx create mode 100644 src/components/EventsTable/EventsTableWrapper.tsx create mode 100644 src/components/ui/table.tsx diff --git a/src/components/EventsTable/EventsTable.tsx b/src/components/EventsTable/EventsTable.tsx new file mode 100644 index 00000000..09caf42a --- /dev/null +++ b/src/components/EventsTable/EventsTable.tsx @@ -0,0 +1,248 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import type { Event } from '@/payload-types' +import { ChevronDown, ChevronsUpDown, ChevronUp } from 'lucide-react' +import { useMemo, useState } from 'react' + +export function EventTable({ events = [] }: { events: Event[] }) { + const [sortConfig, setSortConfig] = useState({ key: 'startDate', direction: 'asc' }) + const [displayCount, setDisplayCount] = useState(10) + + const ITEMS_PER_LOAD = 10 + + // Determine status based on event data + const getStatus = (event: Event) => { + // Check if registration deadline has passed + if (event.registrationDeadline) { + const deadline = new Date(event.registrationDeadline) + if (deadline < new Date()) { + return { label: 'Closed', color: 'bg-slate-600' } + } + } + + // Check if event has passed + if (event.startDate) { + const startDate = new Date(event.startDate) + if (startDate < new Date()) { + return { label: 'Past', color: 'bg-gray-400' } + } + } + + if (event.cost && event.cost > 0) { + return { label: 'Paid', color: 'bg-slate-600' } + } + if (event.location?.isVirtual) { + return { label: 'Virtual', color: 'bg-blue-500' } + } + return { label: 'Open', color: 'bg-green-500' } + } + + // Format date and time + const formatDateTime = (dateString: string) => { + const date = new Date(dateString) + return { + date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), + time: date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }), + } + } + + // Get display address + const getAddress = (event: Event) => { + if (event.location?.isVirtual) { + return 'Virtual Event' + } + const { address, city, state, zip } = event.location || {} + if (address) return address + if (city && state) return `${city}, ${state}` + if (zip) return zip + return 'TBA' + } + + // Get display location (city/venue) + const getLocation = (event: Event) => { + if (event.location?.isVirtual) { + return 'Virtual' + } + const { placeName, city, state } = event.location || {} + if (placeName) return placeName + if (city && state) return `${city}, ${state}` + return 'TBA' + } + + // Get in-person status for sorting + const getInPersonValue = (event: Event) => { + return !event.location?.isVirtual ? 1 : 0 + } + + // Sort function + const sortedEvents = useMemo(() => { + const sorted = [...events].sort((a, b) => { + let aValue = a[sortConfig.key as keyof Event] + let bValue = b[sortConfig.key as keyof Event] + + // Handle special cases + if (sortConfig.key === 'location') { + aValue = getInPersonValue(a) + bValue = getInPersonValue(b) + } else if (sortConfig.key === 'type') { + aValue = getStatus(a).label + bValue = getStatus(b).label + } + + if (aValue == null) return 1 + if (bValue == null) return -1 + + if (typeof aValue === 'string' && typeof bValue === 'string') { + aValue = aValue.toLowerCase() + bValue = bValue.toLowerCase() + } + + if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1 + if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1 + return 0 + }) + + return sorted + }, [events, sortConfig]) + + const displayedEvents = sortedEvents.slice(0, displayCount) + const hasMore = displayCount < sortedEvents.length + + const handleSort = (key: string) => { + if (sortConfig.key === key) { + setSortConfig({ + key, + direction: sortConfig.direction === 'asc' ? 'desc' : 'asc', + }) + } else { + setSortConfig({ key, direction: 'asc' }) + } + } + + const SortIcon = ({ columnKey }: { columnKey: string }) => { + if (sortConfig.key !== columnKey) { + return + } + return sortConfig.direction === 'asc' ? ( + + ) : ( + + ) + } + + const SortableHeader = ({ label, sortKey }: { label: string; sortKey: string }) => ( + + ) + + if (!events || events.length === 0) { + return
No events found
+ } + + return ( +
+
+ + + + + + + + + + + + + + + + Address + Location + + + + Action + + + + {displayedEvents.map((event) => { + const { date, time } = formatDateTime(event.startDate) + const status = getStatus(event) + const inPerson = !event.location?.isVirtual + + return ( + + {date} + {time} + + + {event.title} + + + + + {status.label} + + + {getAddress(event)} + {getLocation(event)} + + {inPerson && } + + + {event.registrationUrl ? ( + + Register + + ) : ( + + )} + + + ) + })} + +
+
+ + {/* Load More Button */} + {hasMore && ( +
+ +
+ )} + + {/* Results info */} +
+ Showing {displayedEvents.length} of {sortedEvents.length} events +
+
+ ) +} diff --git a/src/components/EventsTable/EventsTableWrapper.tsx b/src/components/EventsTable/EventsTableWrapper.tsx new file mode 100644 index 00000000..dc1e7714 --- /dev/null +++ b/src/components/EventsTable/EventsTableWrapper.tsx @@ -0,0 +1,15 @@ +import config from '@payload-config' +import { getPayload } from 'payload' +import { EventTable } from './EventsTable' + +export default async function EventTableWrapper() { + const payload = await getPayload({ config }) + + const { docs: events } = await payload.find({ + collection: 'events', + limit: 1000, + sort: '-startDate', + }) + + return +} diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 00000000..270c7771 --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,91 @@ +import * as React from 'react' + +import { cn } from '@/utilities/ui' + +const Table = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ + + ), +) +Table.displayName = 'Table' + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = 'TableHeader' + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = 'TableBody' + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0', className)} + {...props} + /> +)) +TableFooter.displayName = 'TableFooter' + +const TableRow = React.forwardRef>( + ({ className, ...props }, ref) => ( + + ), +) +TableRow.displayName = 'TableRow' + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = 'TableHead' + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = 'TableCell' + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = 'TableCaption' + +export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow } From 81f8d09a9a97b17c357ad47e5ae621eb2b454188 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Sat, 8 Nov 2025 12:50:12 -0800 Subject: [PATCH 02/48] Add event table block --- src/app/(payload)/admin/importMap.js | 3 + src/blocks/Content/config.ts | 2 + src/blocks/EventTable/Component.tsx | 59 +++++++ src/blocks/EventTable/config.ts | 140 +++++++++++++++++ .../fields/QueriedEventsComponent.tsx | 147 ++++++++++++++++++ .../EventTable/hooks/validateMaxEvents.ts | 15 ++ src/blocks/RenderBlocks.tsx | 3 + src/collections/EventGroups/index.ts | 47 ++++++ src/collections/Pages/index.ts | 12 +- src/components/EventsTable/EventsTable.tsx | 146 ++++++++--------- src/components/RichText/index.tsx | 22 +-- src/payload-types.ts | 96 ++++++++++++ 12 files changed, 591 insertions(+), 101 deletions(-) create mode 100644 src/blocks/EventTable/Component.tsx create mode 100644 src/blocks/EventTable/config.ts create mode 100644 src/blocks/EventTable/fields/QueriedEventsComponent.tsx create mode 100644 src/blocks/EventTable/hooks/validateMaxEvents.ts diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 35e4944e..7e85e6e1 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,6 +1,7 @@ import { QueriedPostsComponent as QueriedPostsComponent_a2ad38d8499118f1bebc7752c0fff51e } from '@/blocks/BlogList/fields/QueriedPostsComponent' import { DefaultColumnAdder as DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01 } from '@/blocks/Content/components/DefaultColumnAdder' import { QueriedEventsComponent as QueriedEventsComponent_65bd30cc675f775ebce6af07a79e525c } from '@/blocks/EventList/fields/QueriedEventsComponent' +import { QueriedEventsComponent as QueriedEventsComponent_f2dcb9815766a4294d50909b2f64da85 } from '@/blocks/EventTable/fields/QueriedEventsComponent' import { SponsorsLayoutDescription as SponsorsLayoutDescription_6f00823041b5b0999b9929fb565110de } from '@/blocks/SponsorsBlock/components/SponsorsLayoutDescription' import { CourseTypeField as CourseTypeField_348fff62462d32a00f93a0ac5be86e99 } from '@/collections/Courses/components/CourseTypeField' import { DuplicatePageFor as DuplicatePageFor_8f1d8961a356bec6784e5c591c016925 } from '@/collections/Pages/components/DuplicatePageFor' @@ -106,6 +107,8 @@ export const importMap = { DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01, '@/blocks/EventList/fields/QueriedEventsComponent#QueriedEventsComponent': QueriedEventsComponent_65bd30cc675f775ebce6af07a79e525c, + '@/blocks/EventTable/fields/QueriedEventsComponent#QueriedEventsComponent': + QueriedEventsComponent_f2dcb9815766a4294d50909b2f64da85, '@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, '@payloadcms/plugin-seo/client#MetaImageComponent': diff --git a/src/blocks/Content/config.ts b/src/blocks/Content/config.ts index c80f34e9..c2ff7bcf 100644 --- a/src/blocks/Content/config.ts +++ b/src/blocks/Content/config.ts @@ -12,6 +12,7 @@ import { ButtonBlock } from '../Button/config' import { CalloutBlock } from '../Callout/config' import { DocumentBlock } from '../DocumentBlock/config' import { EventListBlockLexical } from '../EventList/config' +import { EventTableBlock } from '../EventTable/config' import { GenericEmbedLexical } from '../GenericEmbed/config' import { HeaderBlock } from '../Header/config' import { MediaBlockLexical } from '../MediaBlock/config' @@ -131,6 +132,7 @@ export const Content: Block = { CalloutBlock, DocumentBlock, EventListBlockLexical, + EventTableBlock, SingleEventBlockLexical, GenericEmbedLexical, HeaderBlock, diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx new file mode 100644 index 00000000..b92ebcc8 --- /dev/null +++ b/src/blocks/EventTable/Component.tsx @@ -0,0 +1,59 @@ +import { EventTable } from '@/components/EventsTable/EventsTable' +import RichText from '@/components/RichText' +import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' +import { cn } from '@/utilities/ui' + +type EventTableComponentProps = EventTableBlockProps & { + className?: string +} + +export const EventTableBlockComponent = async (args: EventTableComponentProps) => { + const { heading, belowHeadingContent, className } = args + + const { filterByEventTypes, queriedEvents } = args.dynamicOptions || {} + const { staticEvents } = args.staticOptions || {} + + let events = staticEvents?.filter( + (event): event is Event => typeof event === 'object' && event !== null, + ) + + if (!staticEvents || (staticEvents.length === 0 && queriedEvents && queriedEvents.length > 0)) { + events = queriedEvents?.filter( + (event): event is Event => typeof event === 'object' && event !== null, + ) + } + + const eventsLinkQueryParams = new URLSearchParams() + + if (filterByEventTypes && filterByEventTypes.length > 0) { + eventsLinkQueryParams.set('types', filterByEventTypes.join(',')) + } + + if (!events) { + return null + } + + return ( +
+
+ {heading && ( +
+

{heading}

+
+ )} + {belowHeadingContent && ( +
+ +
+ )} +
+
+ {events && events?.length > 0 ? ( + + ) : ( +

There are no events matching these results.

+ )} +
+
+ ) +} diff --git a/src/blocks/EventTable/config.ts b/src/blocks/EventTable/config.ts new file mode 100644 index 00000000..769b742d --- /dev/null +++ b/src/blocks/EventTable/config.ts @@ -0,0 +1,140 @@ +import { eventTypesData } from '@/collections/Events/constants' +import { getTenantFilter } from '@/utilities/collectionFilters' +import { + BlocksFeature, + HorizontalRuleFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' +import type { Block, Field, FilterOptionsProps } from 'payload' +import { ButtonBlock } from '../Button/config' +import { GenericEmbedLexical } from '../GenericEmbed/config' +import { MediaBlockLexical } from '../MediaBlock/config' +import { validateMaxEvents } from './hooks/validateMaxEvents' + +const defaultStylingFields: Field[] = [ + { name: 'heading', type: 'text' }, + { + name: 'belowHeadingContent', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + BlocksFeature({ + blocks: [ButtonBlock, MediaBlockLexical, GenericEmbedLexical], + }), + HorizontalRuleFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: 'Content Below Heading', + admin: { + description: 'Optional content to display below the heading and above the event table.', + }, + }, + { + type: 'radio', + name: 'eventOptions', + label: 'How do you want to choose your events?', + defaultValue: 'dynamic', + required: true, + options: [ + { + label: 'Do it for me', + value: 'dynamic', + }, + { + label: 'Let me choose', + value: 'static', + }, + ], + }, +] + +const dynamicEventRelatedFields: Field[] = [ + { + name: 'dynamicOptions', + type: 'group', + admin: { + condition: (_, siblingData) => siblingData?.eventOptions === 'dynamic', + }, + fields: [ + { + name: 'filterByEventTypes', + type: 'select', + dbName: 'filterByEventTypes', + options: eventTypesData.map((type) => ({ + label: type.label, + value: type.value, + })), + hasMany: true, + label: 'Filter by Event Type(s)', + admin: { + description: 'Optionally select event types to filter events.', + }, + }, + { + name: 'maxEvents', + type: 'number', + label: 'Max Events Displayed', + min: 1, + max: 20, + defaultValue: 4, + admin: { + description: 'Maximum number of events that will be displayed. Must be an integer.', + step: 1, + }, + hooks: { + beforeValidate: [validateMaxEvents], + }, + }, + { + name: 'queriedEvents', + type: 'relationship', + label: 'Preview Events Order', + relationTo: 'events', + hasMany: true, + admin: { + readOnly: true, + components: { + Field: '@/blocks/EventTable/fields/QueriedEventsComponent#QueriedEventsComponent', + }, + }, + }, + ], + }, +] + +const staticEventRelatedFields: Field[] = [ + { + name: 'staticOptions', + type: 'group', + admin: { + condition: (_, siblingData) => siblingData?.eventOptions === 'static', + }, + fields: [ + { + name: 'staticEvents', + type: 'relationship', + label: 'Choose events', + relationTo: 'events', + hasMany: true, + admin: { + description: 'Choose new event from dropdown and/or drag and drop to change order', + }, + filterOptions: (props: FilterOptionsProps) => ({ + and: [getTenantFilter(props)], + }), + }, + ], + }, +] + +export const EventTableBlock: Block = { + slug: 'eventTable', + interfaceName: 'EventTableBlock', + imageURL: '/thumbnail/EventTableThumbnail.jpg', + fields: [...defaultStylingFields, ...dynamicEventRelatedFields, ...staticEventRelatedFields], +} diff --git a/src/blocks/EventTable/fields/QueriedEventsComponent.tsx b/src/blocks/EventTable/fields/QueriedEventsComponent.tsx new file mode 100644 index 00000000..0d47f286 --- /dev/null +++ b/src/blocks/EventTable/fields/QueriedEventsComponent.tsx @@ -0,0 +1,147 @@ +'use client' + +import { Event } from '@/payload-types' +import { useTenantSelection } from '@/providers/TenantSelectionProvider/index.client' +import { FieldDescription, SelectInput, useField, useForm, useFormFields } from '@payloadcms/ui' +import { OptionObject } from 'payload' +import { useEffect, useState } from 'react' + +type QueriedEventsComponentProps = { + path: string + field?: { + label?: string + } +} + +export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentProps) => { + const label = field?.label || 'Queried Events' + const parentPathParts = path.split('.').slice(0, -1) + + const { value, setValue } = useField({ path }) + const [isLoading, setIsLoading] = useState(false) + const [fetchedEvents, setFetchedEvents] = useState([]) + const [selectOptions, setSelectOptions] = useState([]) + const { setDisabled } = useForm() + + const filterByEventTypes = useFormFields( + ([fields]) => fields[parentPathParts.concat(['filterByEventTypes']).join('.')]?.value, + ) + + const sortBy = useFormFields( + ([fields]) => fields[parentPathParts.concat(['sortBy']).join('.')]?.value, + ) + const maxEvents = useFormFields( + ([fields]) => fields[parentPathParts.concat(['maxEvents']).join('.')]?.value, + ) + const showUpcomingOnly = useFormFields( + ([fields]) => fields[parentPathParts.concat(['showUpcomingOnly']).join('.')]?.value, + ) + const { selectedTenantID: tenant } = useTenantSelection() + + useEffect(() => { + if (!tenant) { + return + } + + const fetchEvents = async () => { + setIsLoading(true) + setDisabled(true) + + try { + const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id + if (!tenantId) return + + const params = new URLSearchParams({ + limit: String(maxEvents || 4), + depth: '1', + 'where[tenant][equals]': String(tenantId), + }) + + if (sortBy) { + params.append('sort', String(sortBy)) + } + + if (showUpcomingOnly) { + params.append('where[startDate][greater_than]', new Date().toISOString()) + } + + if ( + filterByEventTypes && + Array.isArray(filterByEventTypes) && + filterByEventTypes.length > 0 + ) { + const typeIds = filterByEventTypes.filter(Boolean) + + if (typeIds.length > 0) { + params.append('where[type][in]', typeIds.join(',')) + } + } + + const response = await fetch(`/api/events?${params.toString()}`) + if (!response.ok) { + throw new Error('Failed to fetch events') + } + + const data = await response.json() + const events = data.docs || [] + setFetchedEvents(events) + + const options: OptionObject[] = events.map((event: Event) => ({ + label: event.title, + value: String(event.id), + })) + setSelectOptions(options) + + const currentEventIds = (value || []) + .map((event) => (typeof event === 'number' ? event : event.id)) + .sort() + const newEventIds = events.map((event: Event) => event.id).sort() + + if (JSON.stringify(currentEventIds) !== JSON.stringify(newEventIds)) { + setValue(events) + } + } catch (error) { + console.error('Error fetching events for EventTable block:', error) + setFetchedEvents([]) + setSelectOptions([]) + setValue([]) + } finally { + setIsLoading(false) + setDisabled(false) + } + } + + fetchEvents() + }, [ + filterByEventTypes, + sortBy, + maxEvents, + showUpcomingOnly, + tenant, + setValue, + value, + setDisabled, + ]) + + const currentValue = fetchedEvents.map((event) => String(event.id)) + + return ( +
+ + +
+ ) +} diff --git a/src/blocks/EventTable/hooks/validateMaxEvents.ts b/src/blocks/EventTable/hooks/validateMaxEvents.ts new file mode 100644 index 00000000..0f4ac8fb --- /dev/null +++ b/src/blocks/EventTable/hooks/validateMaxEvents.ts @@ -0,0 +1,15 @@ +import type { FieldHook } from 'payload' + +export const validateMaxEvents: FieldHook = ({ value }) => { + if (value === undefined || value === null) { + return value + } + + const numValue = typeof value === 'string' ? parseFloat(value) : value + + if (!Number.isInteger(numValue)) { + return Math.floor(numValue) + } + + return numValue +} diff --git a/src/blocks/RenderBlocks.tsx b/src/blocks/RenderBlocks.tsx index e2fdd1ef..d325a15e 100644 --- a/src/blocks/RenderBlocks.tsx +++ b/src/blocks/RenderBlocks.tsx @@ -8,6 +8,7 @@ import { BlogListBlockComponent } from '@/blocks/BlogList/Component' import { ContentBlock } from '@/blocks/Content/Component' import { DocumentBlock } from '@/blocks/DocumentBlock/Component' import { EventListBlockComponent } from '@/blocks/EventList/Component' +import { EventTableBlockComponent } from '@/blocks/EventTable/Component' import { FormBlock } from '@/blocks/Form/Component' import { GenericEmbedBlock } from '@/blocks/GenericEmbed/Component' import { HeaderBlockComponent } from '@/blocks/Header/Component' @@ -58,6 +59,8 @@ export const RenderBlock = ({ block, payload }: { block: Page['layout'][0]; payl return case 'eventList': return + case 'eventTable': + return case 'formBlock': return case 'genericEmbed': diff --git a/src/collections/EventGroups/index.ts b/src/collections/EventGroups/index.ts index 382e6afc..a738755a 100644 --- a/src/collections/EventGroups/index.ts +++ b/src/collections/EventGroups/index.ts @@ -5,6 +5,25 @@ import { slugField } from '@/fields/slug' import { tenantField } from '@/fields/tenantField' import { CollectionConfig } from 'payload' +import { + BlocksFeature, + HorizontalRuleFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' + +import { DocumentBlock } from '@/blocks/DocumentBlock/config' +import { EventListBlockLexical } from '@/blocks/EventList/config' +import { GenericEmbedLexical } from '@/blocks/GenericEmbed/config' +import { HeaderBlock } from '@/blocks/Header/config' +import { MediaBlockLexical } from '@/blocks/MediaBlock/config' +import { SingleEventBlockLexical } from '@/blocks/SingleEvent/config' +import { SponsorsBlock } from '@/blocks/SponsorsBlock/config' + +import { ButtonBlock } from '@/blocks/Button/config' +import { CalloutBlock } from '@/blocks/Callout/config' +import { EventTableBlock } from '@/blocks/EventTable/config' + export const EventGroups: CollectionConfig = { slug: 'eventGroups', access: accessByTenantRole('eventGroups'), @@ -24,6 +43,34 @@ export const EventGroups: CollectionConfig = { name: 'description', type: 'textarea', }, + { + name: 'richText', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + BlocksFeature({ + blocks: [ + ButtonBlock, + CalloutBlock, + DocumentBlock, + EventListBlockLexical, + EventTableBlock, + SingleEventBlockLexical, + GenericEmbedLexical, + HeaderBlock, + MediaBlockLexical, + SponsorsBlock, + ], + }), + HorizontalRuleFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: false, + }, slugField(), contentHashField(), ], diff --git a/src/collections/Pages/index.ts b/src/collections/Pages/index.ts index a15af828..c25839db 100644 --- a/src/collections/Pages/index.ts +++ b/src/collections/Pages/index.ts @@ -1,8 +1,12 @@ +import { Tenant } from '@/payload-types' import type { CollectionConfig } from 'payload' import { BiographyBlock } from '@/blocks/Biography/config' import { BlogListBlock } from '@/blocks/BlogList/config' import { Content } from '@/blocks/Content/config' +import { DocumentBlock } from '@/blocks/DocumentBlock/config' +import { EventListBlock } from '@/blocks/EventList/config' +import { EventTableBlock } from '@/blocks/EventTable/config' import { FormBlock } from '@/blocks/Form/config' import { GenericEmbed } from '@/blocks/GenericEmbed/config' import { HeaderBlock } from '@/blocks/Header/config' @@ -13,6 +17,8 @@ import { ImageTextList } from '@/blocks/ImageTextList/config' import { LinkPreviewBlock } from '@/blocks/LinkPreview/config' import { MediaBlock } from '@/blocks/MediaBlock/config' import { SingleBlogPostBlock } from '@/blocks/SingleBlogPost/config' +import { SingleEventBlock } from '@/blocks/SingleEvent/config' +import { SponsorsBlock } from '@/blocks/SponsorsBlock/config' import { TeamBlock } from '@/blocks/Team/config' import { populatePublishedAt } from '@/hooks/populatePublishedAt' @@ -32,12 +38,7 @@ import { PreviewField, } from '@payloadcms/plugin-seo/fields' -import { DocumentBlock } from '@/blocks/DocumentBlock/config' -import { EventListBlock } from '@/blocks/EventList/config' -import { SingleEventBlock } from '@/blocks/SingleEvent/config' -import { SponsorsBlock } from '@/blocks/SponsorsBlock/config' import { duplicatePageToTenant } from '@/collections/Pages/endpoints/duplicatePageToTenant' -import { Tenant } from '@/payload-types' export const Pages: CollectionConfig<'pages'> = { slug: 'pages', @@ -122,6 +123,7 @@ export const Pages: CollectionConfig<'pages'> = { Content, DocumentBlock, EventListBlock, + EventTableBlock, SingleEventBlock, FormBlock, HeaderBlock, diff --git a/src/components/EventsTable/EventsTable.tsx b/src/components/EventsTable/EventsTable.tsx index 09caf42a..6b115256 100644 --- a/src/components/EventsTable/EventsTable.tsx +++ b/src/components/EventsTable/EventsTable.tsx @@ -58,7 +58,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { // Get display address const getAddress = (event: Event) => { if (event.location?.isVirtual) { - return 'Virtual Event' + return '' } const { address, city, state, zip } = event.location || {} if (address) return address @@ -70,7 +70,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { // Get display location (city/venue) const getLocation = (event: Event) => { if (event.location?.isVirtual) { - return 'Virtual' + return 'Online' } const { placeName, city, state } = event.location || {} if (placeName) return placeName @@ -78,11 +78,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { return 'TBA' } - // Get in-person status for sorting - const getInPersonValue = (event: Event) => { - return !event.location?.isVirtual ? 1 : 0 - } - // Sort function const sortedEvents = useMemo(() => { const sorted = [...events].sort((a, b) => { @@ -90,10 +85,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { let bValue = b[sortConfig.key as keyof Event] // Handle special cases - if (sortConfig.key === 'location') { - aValue = getInPersonValue(a) - bValue = getInPersonValue(b) - } else if (sortConfig.key === 'type') { + if (sortConfig.key === 'type') { aValue = getStatus(a).label bValue = getStatus(b).label } @@ -155,77 +147,69 @@ export function EventTable({ events = [] }: { events: Event[] }) { return (
-
- - - - - - - - - - - - - - - - Address - Location - - - - Action - - - - {displayedEvents.map((event) => { - const { date, time } = formatDateTime(event.startDate) - const status = getStatus(event) - const inPerson = !event.location?.isVirtual - - return ( - - {date} - {time} - - - {event.title} - - - - + + + + + + + + + + + + + + + Address + Location + Virtual + Action + + + + {displayedEvents.map((event) => { + const { date, time } = formatDateTime(event.startDate) + const status = getStatus(event) + const isVirtual = event.location?.isVirtual + + return ( + + {date} + {time} + {event.title} + + + {status.label} + + + {getAddress(event)} + {getLocation(event)} + + {isVirtual && } + + + {event.registrationUrl ? ( + - {status.label} - - - {getAddress(event)} - {getLocation(event)} - - {inPerson && } - - - {event.registrationUrl ? ( - - Register - - ) : ( - - )} - - - ) - })} - -
-
+ Register + + ) : ( + + )} + + + ) + })} + +
{/* Load More Button */} {hasMore && ( diff --git a/src/components/RichText/index.tsx b/src/components/RichText/index.tsx index de4919bb..7aa5cb0c 100644 --- a/src/components/RichText/index.tsx +++ b/src/components/RichText/index.tsx @@ -17,6 +17,7 @@ import { ButtonBlockComponent } from '@/blocks/Button/Component' import { CalloutBlock } from '@/blocks/Callout/Component' import { DocumentBlock } from '@/blocks/DocumentBlock/Component' import { EventListBlockComponent } from '@/blocks/EventList/Component' +import { EventTableBlockComponent } from '@/blocks/EventTable/Component' import { GenericEmbedBlock } from '@/blocks/GenericEmbed/Component' import { HeaderBlockComponent } from '@/blocks/Header/Component' import { SingleBlogPostBlockComponent } from '@/blocks/SingleBlogPost/Component' @@ -29,6 +30,7 @@ import type { CalloutBlock as CalloutBlockProps, DocumentBlock as DocumentBlockProps, EventListBlock as EventListBlockProps, + EventTableBlock as EventTableBlockProps, GenericEmbedBlock as GenericEmbedBlockProps, HeaderBlock as HeaderBlockProps, MediaBlock as MediaBlockProps, @@ -47,6 +49,7 @@ type NodeTypes = | CalloutBlockProps | DocumentBlockProps | EventListBlockProps + | EventTableBlockProps | GenericEmbedBlockProps | HeaderBlockProps | MediaBlockProps @@ -67,44 +70,35 @@ const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => { const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ ...defaultConverters, ...LinkJSXConverter({ internalDocToHref }), + // if block has two variants - to make TS happy we fallback to the default for the block variant blocks: { banner: ({ node }) => , blogList: ({ node }) => ( ), buttonBlock: ({ node }) => , calloutBlock: ({ node }) => , documentBlock: ({ node }) => ( - + ), eventList: ({ node }) => ( ), + eventTable: ({ node }) => , singleEvent: ({ node }) => ( ), genericEmbed: ({ node }) => ( - + ), headerBlock: ({ node }) => , mediaBlock: ({ node }) => ( @@ -112,7 +106,6 @@ const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) className="col-start-1 col-span-3" imgClassName="m-0" {...node.fields} - // src/blocks/MediaBlock/config.ts has two variants - to make TS happy we fallback to the default for the MediaBlockLexical variant wrapInContainer={node.fields.wrapInContainer || false} captionClassName="mx-auto max-w-[48rem]" /> @@ -120,7 +113,6 @@ const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) singleBlogPost: ({ node }) => ( ), diff --git a/src/payload-types.ts b/src/payload-types.ts index cec7db99..a2a74fdc 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -316,6 +316,7 @@ export interface Page { | ContentBlock | DocumentBlock | EventListBlock + | EventTableBlock | SingleEventBlock | FormBlock | HeaderBlock @@ -911,6 +912,21 @@ export interface EventGroup { tenant: number | Tenant; title: string; description?: string | null; + richText?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; slug: string; contentHash?: string | null; updatedAt: string; @@ -930,6 +946,61 @@ export interface EventTag { updatedAt: string; createdAt: string; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "EventTableBlock". + */ +export interface EventTableBlock { + heading?: string | null; + /** + * Optional content to display below the heading and above the event table. + */ + belowHeadingContent?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + eventOptions: 'dynamic' | 'static'; + dynamicOptions?: { + /** + * Optionally select event types to filter events. + */ + filterByEventTypes?: + | ( + | 'events-by-ac' + | 'awareness' + | 'workshop' + | 'field-class-by-ac' + | 'course-by-external-provider' + | 'volunteer' + )[] + | null; + /** + * Maximum number of events that will be displayed. Must be an integer. + */ + maxEvents?: number | null; + queriedEvents?: (number | Event)[] | null; + }; + staticOptions?: { + /** + * Choose new event from dropdown and/or drag and drop to change order + */ + staticEvents?: (number | Event)[] | null; + }; + id?: string | null; + blockName?: string | null; + blockType: 'eventTable'; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "SingleEventBlock". @@ -3041,6 +3112,7 @@ export interface PagesSelect { content?: T | ContentBlockSelect; documentBlock?: T | DocumentBlockSelect; eventList?: T | EventListBlockSelect; + eventTable?: T | EventTableBlockSelect; singleEvent?: T | SingleEventBlockSelect; formBlock?: T | FormBlockSelect; headerBlock?: T | HeaderBlockSelect; @@ -3068,6 +3140,29 @@ export interface PagesSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "EventTableBlock_select". + */ +export interface EventTableBlockSelect { + heading?: T; + belowHeadingContent?: T; + eventOptions?: T; + dynamicOptions?: + | T + | { + filterByEventTypes?: T; + maxEvents?: T; + queriedEvents?: T; + }; + staticOptions?: + | T + | { + staticEvents?: T; + }; + id?: T; + blockName?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "posts_select". @@ -3308,6 +3403,7 @@ export interface EventGroupsSelect { tenant?: T; title?: T; description?: T; + richText?: T; slug?: T; contentHash?: T; updatedAt?: T; From 2438dfad39634b4bf7f15144aaf958aa7cbbc389 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 14:22:17 -0800 Subject: [PATCH 03/48] Add the ability to build out an events group page --- .../(frontend)/[center]/event/[slug]/page.tsx | 131 ++++++++++++++++++ .../[center]/events/[slug]/page.tsx | 30 ++-- src/collections/EventGroups/index.ts | 116 +++++++++++----- src/endpoints/seed/events.ts | 44 +----- src/endpoints/seed/index.ts | 45 +++--- src/endpoints/seed/utilities.ts | 43 ++++++ src/payload-types.ts | 10 +- 7 files changed, 302 insertions(+), 117 deletions(-) create mode 100644 src/app/(frontend)/[center]/event/[slug]/page.tsx diff --git a/src/app/(frontend)/[center]/event/[slug]/page.tsx b/src/app/(frontend)/[center]/event/[slug]/page.tsx new file mode 100644 index 00000000..d34a9407 --- /dev/null +++ b/src/app/(frontend)/[center]/event/[slug]/page.tsx @@ -0,0 +1,131 @@ +import RichText from '@/components/RichText' +import configPromise from '@payload-config' +import { draftMode } from 'next/headers' +import { getPayload } from 'payload' + +import { LivePreviewListener } from '@/components/LivePreviewListener' +import { Media } from '@/components/Media' +import { generateMetaForEvent } from '@/utilities/generateMeta' +import { cn } from '@/utilities/ui' +import { Metadata, ResolvedMetadata } from 'next' + +export const dynamicParams = true + +export async function generateStaticParams() { + const payload = await getPayload({ config: configPromise }) + const events = await payload.find({ + collection: 'events', + limit: 1000, + pagination: false, + depth: 3, + select: { + tenant: true, + slug: true, + }, + }) + + const params: PathArgs[] = [] + for (const event of events.docs) { + if (typeof event.tenant === 'number') { + payload.logger.error(`got number for event tenant`) + continue + } + if (event.tenant) { + params.push({ center: event.tenant.slug, slug: event.slug }) + } + } + + return params +} + +type Args = { + params: Promise +} + +type PathArgs = { + center: string + slug: string +} + +export default async function EventGroup({ params: paramsPromise }: Args) { + const { isEnabled: draft } = await draftMode() + const { center, slug } = await paramsPromise + const event = await queryEventBySlug({ center: center, slug: slug }) + + return ( +
+ {draft && } + +
+ {event.featuredImage && ( + + )} +
+
+
+
+

{event.title}

+
+
{event.description}
+
+
+ + {event.content && ( + + )} +
+
+
+ ) +} + +export async function generateMetadata( + { params: paramsPromise }: Args, + parent: Promise, +): Promise { + const parentMeta = (await parent) as Metadata + const { center, slug = '' } = await paramsPromise + const event = await queryEventBySlug({ center: center, slug: slug }) + + return generateMetaForEvent({ center: center, doc: event, parentMeta }) +} + +const queryEventBySlug = async ({ center, slug }: { center: string; slug: string }) => { + const { isEnabled: draft } = await draftMode() + + const payload = await getPayload({ config: configPromise }) + + const result = await payload.find({ + collection: 'eventGroups', + draft, + limit: 1, + pagination: false, + populate: { + tenants: { + slug: true, + name: true, + customDomain: true, + }, + }, + where: { + and: [ + { + 'tenant.slug': { + equals: center, + }, + }, + { + slug: { + equals: slug, + }, + }, + ], + }, + }) + + return result.docs?.[0] || null +} diff --git a/src/app/(frontend)/[center]/events/[slug]/page.tsx b/src/app/(frontend)/[center]/events/[slug]/page.tsx index 2b68b2f7..86f68109 100644 --- a/src/app/(frontend)/[center]/events/[slug]/page.tsx +++ b/src/app/(frontend)/[center]/events/[slug]/page.tsx @@ -2,7 +2,7 @@ import { Redirects } from '@/components/Redirects' import RichText from '@/components/RichText' import configPromise from '@payload-config' import { draftMode } from 'next/headers' -import { getPayload, Where } from 'payload' +import { getPayload } from 'payload' import { EventInfo } from '@/components/EventInfo' import { LivePreviewListener } from '@/components/LivePreviewListener' @@ -170,19 +170,6 @@ const queryEventBySlug = async ({ center, slug }: { center: string; slug: string const payload = await getPayload({ config: configPromise }) - const conditions: Where[] = [ - { - 'tenant.slug': { - equals: center, - }, - }, - { - slug: { - equals: slug, - }, - }, - ] - const result = await payload.find({ collection: 'events', draft, @@ -195,7 +182,20 @@ const queryEventBySlug = async ({ center, slug }: { center: string; slug: string customDomain: true, }, }, - where: { and: conditions }, + where: { + and: [ + { + 'tenant.slug': { + equals: center, + }, + }, + { + slug: { + equals: slug, + }, + }, + ], + }, }) return result.docs?.[0] || null diff --git a/src/collections/EventGroups/index.ts b/src/collections/EventGroups/index.ts index a738755a..cbffd775 100644 --- a/src/collections/EventGroups/index.ts +++ b/src/collections/EventGroups/index.ts @@ -3,8 +3,10 @@ import { filterByTenant } from '@/access/filterByTenant' import { contentHashField } from '@/fields/contentHashField' import { slugField } from '@/fields/slug' import { tenantField } from '@/fields/tenantField' +import { getImageTypeFilter } from '@/utilities/collectionFilters' import { CollectionConfig } from 'payload' +import { MetaImageField } from '@payloadcms/plugin-seo/fields' import { BlocksFeature, HorizontalRuleFeature, @@ -12,18 +14,20 @@ import { lexicalEditor, } from '@payloadcms/richtext-lexical' +import { Banner } from '@/blocks/Banner/config' +import { BlogListBlockLexical } from '@/blocks/BlogList/config' +import { ButtonBlock } from '@/blocks/Button/config' +import { CalloutBlock } from '@/blocks/Callout/config' import { DocumentBlock } from '@/blocks/DocumentBlock/config' import { EventListBlockLexical } from '@/blocks/EventList/config' +import { EventTableBlock } from '@/blocks/EventTable/config' import { GenericEmbedLexical } from '@/blocks/GenericEmbed/config' import { HeaderBlock } from '@/blocks/Header/config' import { MediaBlockLexical } from '@/blocks/MediaBlock/config' +import { SingleBlogPostBlockLexical } from '@/blocks/SingleBlogPost/config' import { SingleEventBlockLexical } from '@/blocks/SingleEvent/config' import { SponsorsBlock } from '@/blocks/SponsorsBlock/config' -import { ButtonBlock } from '@/blocks/Button/config' -import { CalloutBlock } from '@/blocks/Callout/config' -import { EventTableBlock } from '@/blocks/EventTable/config' - export const EventGroups: CollectionConfig = { slug: 'eventGroups', access: accessByTenantRole('eventGroups'), @@ -35,43 +39,83 @@ export const EventGroups: CollectionConfig = { fields: [ tenantField(), { - name: 'title', - type: 'text', - required: true, - }, - { - name: 'description', - type: 'textarea', + type: 'group', + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + { + name: 'description', + type: 'textarea', + }, + ], }, { - name: 'richText', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - BlocksFeature({ - blocks: [ - ButtonBlock, - CalloutBlock, - DocumentBlock, - EventListBlockLexical, - EventTableBlock, - SingleEventBlockLexical, - GenericEmbedLexical, - HeaderBlock, - MediaBlockLexical, - SponsorsBlock, - ], - }), - HorizontalRuleFeature(), - InlineToolbarFeature(), - ] + type: 'group', + label: 'Landing Page Content', + admin: { + description: + "Create page content for this event's landing page. This landing page will only be displayed if there is not an External Event URL.", + }, + fields: [ + { + name: 'content', + label: '', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + BlocksFeature({ + blocks: [ + Banner, + BlogListBlockLexical, + ButtonBlock, + CalloutBlock, + DocumentBlock, + EventListBlockLexical, + EventTableBlock, + SingleEventBlockLexical, + GenericEmbedLexical, + HeaderBlock, + MediaBlockLexical, + SingleBlogPostBlockLexical, + SponsorsBlock, + ], + }), + HorizontalRuleFeature(), + InlineToolbarFeature(), + ] + }, + }), + required: true, }, - }), - label: false, + ], }, slugField(), + MetaImageField({ + hasGenerateFn: true, + relationTo: 'media', + overrides: { + admin: { + allowCreate: true, + position: 'sidebar', + }, + name: 'featuredImage', + label: 'Featured image', + }, + }), + { + name: 'thumbnailImage', + type: 'upload', + relationTo: 'media', + filterOptions: getImageTypeFilter, + admin: { + position: 'sidebar', + }, + }, contentHashField(), ], } diff --git a/src/endpoints/seed/events.ts b/src/endpoints/seed/events.ts index a8bdf042..a34361da 100644 --- a/src/endpoints/seed/events.ts +++ b/src/endpoints/seed/events.ts @@ -1,55 +1,13 @@ import { Media, Tenant } from '@/payload-types' import { US_TIMEZONES } from '@/utilities/timezones' import { RequiredDataFromCollectionSlug } from 'payload' +import { futureDate, simpleContent } from './utilities' export const getEventsData = ( tenant: Tenant, featuredImage?: Media, thumbnailImage?: Media, ): RequiredDataFromCollectionSlug<'events'>[] => { - // Helper to create dates in the future - const futureDate = (monthOffset: number, day: number, hour: number = 18) => { - const date = new Date() - date.setMonth(date.getMonth() + monthOffset) - date.setDate(day) - date.setHours(hour, 0, 0, 0) - return date.toISOString() - } - - // Helper to create properly formatted lexical text nodes - const textNode = (text: string) => ({ - type: 'text', - detail: 0, - format: 0, - mode: 'normal' as const, - style: '', - text, - version: 1, - }) - - // Helper to create properly formatted lexical paragraph nodes - const paragraphNode = (text: string) => ({ - type: 'paragraph', - children: [textNode(text)], - direction: 'ltr' as const, - format: '' as '' | 'left' | 'start' | 'center' | 'right' | 'end' | 'justify', - indent: 0, - textFormat: 0, - version: 1, - }) - - // Helper to create simple content with just paragraphs - const simpleContent = (...paragraphs: string[]) => ({ - root: { - type: 'root', - children: paragraphs.map(paragraphNode), - direction: 'ltr' as const, - format: '' as '' | 'left' | 'start' | 'center' | 'right' | 'end' | 'justify', - indent: 0, - version: 1, - }, - }) - return [ // Awareness event - Free beginner-friendly { diff --git a/src/endpoints/seed/index.ts b/src/endpoints/seed/index.ts index d75b50a6..2978e10a 100644 --- a/src/endpoints/seed/index.ts +++ b/src/endpoints/seed/index.ts @@ -1,6 +1,6 @@ import { page } from '@/endpoints/seed/pages/page' import { upsert, upsertGlobals } from '@/endpoints/seed/upsert' -import { getPath, getSeedImageByFilename } from '@/endpoints/seed/utilities' +import { getPath, getSeedImageByFilename, simpleContent } from '@/endpoints/seed/utilities' import { Form, Tenant } from '@/payload-types' import fs from 'fs' import { headers } from 'next/headers' @@ -307,25 +307,6 @@ export const seed = async ({ }, ]) - // Event groups - await upsert( - 'eventGroups', - payload, - incremental, - tenantsById, - (obj) => obj.slug, - Object.values(tenants) - .map((tenant): RequiredDataFromCollectionSlug<'eventGroups'>[] => [ - { - title: 'Meet Your Forecaster', - description: 'Meet your local avalanche forecasters & learn more about your avy center', - slug: 'meet-your-forecaster', - tenant: tenant.id, - }, - ]) - .flat(), - ) - // Event tags await upsert( 'eventTags', @@ -910,6 +891,30 @@ export const seed = async ({ }) .flat(), ) + // Event groups + await upsert( + 'eventGroups', + payload, + incremental, + tenantsById, + (obj) => obj.slug, + Object.values(tenants) + .map((tenant): RequiredDataFromCollectionSlug<'eventGroups'>[] => [ + { + title: 'Meet Your Forecaster', + description: 'Meet your local avalanche forecasters & learn more about your avy center', + slug: 'meet-your-forecaster', + tenant: tenant.id, + content: simpleContent( + 'Learn the basics of avalanche safety in this free community presentation. Topics include: understanding avalanche terrain, reading avalanche forecasts, essential rescue equipment, and trip planning basics.', + 'No previous experience necessary. This event is open to all.', + ), + thumbnailImage: images[tenant.slug]['imageMountain'], + featuredImage: images[tenant.slug]['image1'], + }, + ]) + .flat(), + ) payload.logger.info(`— Seeding contact forms...`) diff --git a/src/endpoints/seed/utilities.ts b/src/endpoints/seed/utilities.ts index 2b5a1605..a7851de2 100644 --- a/src/endpoints/seed/utilities.ts +++ b/src/endpoints/seed/utilities.ts @@ -89,3 +89,46 @@ export async function getSeedImageByFilename(filename: string, logger: Logger) { throw error } } + +// Helper to create dates in the future +export const futureDate = (monthOffset: number, day: number, hour: number = 18) => { + const date = new Date() + date.setMonth(date.getMonth() + monthOffset) + date.setDate(day) + date.setHours(hour, 0, 0, 0) + return date.toISOString() +} + +// Helper to create properly formatted lexical text nodes +export const textNode = (text: string) => ({ + type: 'text', + detail: 0, + format: 0, + mode: 'normal' as const, + style: '', + text, + version: 1, +}) + +// Helper to create properly formatted lexical paragraph nodes +export const paragraphNode = (text: string) => ({ + type: 'paragraph', + children: [textNode(text)], + direction: 'ltr' as const, + format: '' as '' | 'left' | 'start' | 'center' | 'right' | 'end' | 'justify', + indent: 0, + textFormat: 0, + version: 1, +}) + +// Helper to create simple content with just paragraphs +export const simpleContent = (...paragraphs: string[]) => ({ + root: { + type: 'root', + children: paragraphs.map(paragraphNode), + direction: 'ltr' as const, + format: '' as '' | 'left' | 'start' | 'center' | 'right' | 'end' | 'justify', + indent: 0, + version: 1, + }, +}) diff --git a/src/payload-types.ts b/src/payload-types.ts index a2a74fdc..764a2f7b 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -912,7 +912,7 @@ export interface EventGroup { tenant: number | Tenant; title: string; description?: string | null; - richText?: { + content: { root: { type: string; children: { @@ -926,8 +926,10 @@ export interface EventGroup { version: number; }; [k: string]: unknown; - } | null; + }; slug: string; + featuredImage?: (number | null) | Media; + thumbnailImage?: (number | null) | Media; contentHash?: string | null; updatedAt: string; createdAt: string; @@ -3403,8 +3405,10 @@ export interface EventGroupsSelect { tenant?: T; title?: T; description?: T; - richText?: T; + content?: T; slug?: T; + featuredImage?: T; + thumbnailImage?: T; contentHash?: T; updatedAt?: T; createdAt?: T; From 122436be402788c77ff02889a6fe492371acdb25 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 16:18:24 -0800 Subject: [PATCH 04/48] Update table to be responsive --- src/components/EventsTable/EventsTable.tsx | 222 +++++++++++++++------ 1 file changed, 158 insertions(+), 64 deletions(-) diff --git a/src/components/EventsTable/EventsTable.tsx b/src/components/EventsTable/EventsTable.tsx index 6b115256..3eff41fc 100644 --- a/src/components/EventsTable/EventsTable.tsx +++ b/src/components/EventsTable/EventsTable.tsx @@ -10,48 +10,57 @@ import { TableRow, } from '@/components/ui/table' import type { Event } from '@/payload-types' -import { ChevronDown, ChevronsUpDown, ChevronUp } from 'lucide-react' +import { format } from 'date-fns' +import { ChevronDown, ChevronRight, ChevronsUpDown, ChevronUp, ExternalLink } from 'lucide-react' import { useMemo, useState } from 'react' +import { CMSLink } from '../Link' export function EventTable({ events = [] }: { events: Event[] }) { const [sortConfig, setSortConfig] = useState({ key: 'startDate', direction: 'asc' }) const [displayCount, setDisplayCount] = useState(10) + const [expandedRows, setExpandedRows] = useState>(new Set()) const ITEMS_PER_LOAD = 10 // Determine status based on event data const getStatus = (event: Event) => { - // Check if registration deadline has passed - if (event.registrationDeadline) { - const deadline = new Date(event.registrationDeadline) - if (deadline < new Date()) { - return { label: 'Closed', color: 'bg-slate-600' } - } - } + const now = new Date() + const isVirtual = event.location?.isVirtual || false + const isPast = event.startDate ? new Date(event.startDate) < now : false + const isRegistrationClosed = event.registrationDeadline + ? new Date(event.registrationDeadline) < now + : false - // Check if event has passed - if (event.startDate) { - const startDate = new Date(event.startDate) - if (startDate < new Date()) { - return { label: 'Past', color: 'bg-gray-400' } - } - } + // Determine label and color + let label = 'Open' + let color = 'bg-brand-400' - if (event.cost && event.cost > 0) { - return { label: 'Paid', color: 'bg-slate-600' } + if (isRegistrationClosed) { + label = 'Closed' + color = 'bg-slate-600' + } else if (isPast) { + label = 'Past' + color = 'bg-gray-400' + } else if (isVirtual) { + label = 'Virtual' + color = 'bg-secondary' } - if (event.location?.isVirtual) { - return { label: 'Virtual', color: 'bg-blue-500' } + + return { + label, + color, + isVirtual, + isPast, + isRegistrationClosed, } - return { label: 'Open', color: 'bg-green-500' } } // Format date and time const formatDateTime = (dateString: string) => { const date = new Date(dateString) return { - date: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), - time: date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }), + date: format(date, 'MMM d, yyyy'), // "Nov 10, 2025" + time: format(date, 'h:mm a'), } } @@ -120,6 +129,16 @@ export function EventTable({ events = [] }: { events: Event[] }) { } } + const toggleRow = (eventId: string) => { + const newExpanded = new Set(expandedRows) + if (newExpanded.has(eventId)) { + newExpanded.delete(eventId) + } else { + newExpanded.add(eventId) + } + setExpandedRows(newExpanded) + } + const SortIcon = ({ columnKey }: { columnKey: string }) => { if (sortConfig.key !== columnKey) { return @@ -146,66 +165,141 @@ export function EventTable({ events = [] }: { events: Event[] }) { } return ( -
+
- + + - - - - + - + - Address - Location - Virtual - Action + Location + + + + {displayedEvents.map((event) => { const { date, time } = formatDateTime(event.startDate) const status = getStatus(event) - const isVirtual = event.location?.isVirtual + const { isVirtual, isPast, isRegistrationClosed } = status + const isExpanded = expandedRows.has(String(event.id)) return ( - - {date} - {time} - {event.title} - - - {status.label} - - - {getAddress(event)} - {getLocation(event)} - - {isVirtual && } - - - {event.registrationUrl ? ( - + { + if (typeof window !== 'undefined' && window.innerWidth < 1024) { + toggleRow(String(event.id)) + } + }} + > + + + + +
+
{date}
+
{time}
+
+
+ + {/* Name */} + {event.title} + + {/* Status label */} + + - Register -
- ) : ( - - )} -
-
+ {status.label} + + + + {/* Location */} + +
+
{getLocation(event)}
+
{getAddress(event)}
+
+
+ + {event.cost === 0 ? 'Free' : `$${event.cost}`} + + + {/* Register button */} + + {event.registrationUrl && !isPast && !isRegistrationClosed ? ( + + Register + + + ) : isPast || isRegistrationClosed ? ( + + ) : ( + + )} + + + + {/* Expanded row for details on smaller screens */} + {isExpanded && ( + + + +
+ {/* Location */} +
+

Location

+

{getLocation(event)}

+ {getAddress(event) && ( +

{getAddress(event)}

+ )} +
+ + {/* Status badge */} +
+
+

Status

+ + {status.label} + +
+ + {isVirtual && ( +
+

Virtual

+

Yes

+
+ )} +
+
+
+
+ )} + ) })}
From 2f6631a148600da9d955f2d6d889453ddeda285b Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 16:26:17 -0800 Subject: [PATCH 05/48] Change file name --- src/blocks/EventTable/Component.tsx | 2 +- src/components/EventsTable/EventsTableWrapper.tsx | 2 +- src/components/EventsTable/{EventsTable.tsx => index.tsx} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/components/EventsTable/{EventsTable.tsx => index.tsx} (100%) diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index b92ebcc8..5151f3e1 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -1,4 +1,4 @@ -import { EventTable } from '@/components/EventsTable/EventsTable' +import { EventTable } from '@/components/EventsTable' import RichText from '@/components/RichText' import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' import { cn } from '@/utilities/ui' diff --git a/src/components/EventsTable/EventsTableWrapper.tsx b/src/components/EventsTable/EventsTableWrapper.tsx index dc1e7714..1e77fda0 100644 --- a/src/components/EventsTable/EventsTableWrapper.tsx +++ b/src/components/EventsTable/EventsTableWrapper.tsx @@ -1,6 +1,6 @@ import config from '@payload-config' import { getPayload } from 'payload' -import { EventTable } from './EventsTable' +import { EventTable } from './index' export default async function EventTableWrapper() { const payload = await getPayload({ config }) diff --git a/src/components/EventsTable/EventsTable.tsx b/src/components/EventsTable/index.tsx similarity index 100% rename from src/components/EventsTable/EventsTable.tsx rename to src/components/EventsTable/index.tsx From 10ef2e2ac937356455888a5922c7b1249fbfb3a4 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 17:49:36 -0800 Subject: [PATCH 06/48] Update seed script for event group and event table --- src/components/EventsTable/index.tsx | 50 ++++++++++++++++--------- src/endpoints/seed/events.ts | 10 ++--- src/endpoints/seed/index.ts | 55 +++++++++++++++++++++++++--- src/endpoints/seed/utilities.ts | 8 ++++ 4 files changed, 96 insertions(+), 27 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index 3eff41fc..dbbadaa1 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -10,7 +10,7 @@ import { TableRow, } from '@/components/ui/table' import type { Event } from '@/payload-types' -import { format } from 'date-fns' +import { addDays, format, isWithinInterval } from 'date-fns' import { ChevronDown, ChevronRight, ChevronsUpDown, ChevronUp, ExternalLink } from 'lucide-react' import { useMemo, useState } from 'react' import { CMSLink } from '../Link' @@ -31,19 +31,23 @@ export function EventTable({ events = [] }: { events: Event[] }) { ? new Date(event.registrationDeadline) < now : false + const isRegistrationClosingSoon = event.registrationDeadline + ? isWithinInterval(new Date(event.registrationDeadline), { + start: new Date(), + end: addDays(new Date(), 7), + }) && !isRegistrationClosed + : false + // Determine label and color let label = 'Open' - let color = 'bg-brand-400' + let color = 'bg-brand-600' if (isRegistrationClosed) { label = 'Closed' - color = 'bg-slate-600' + color = 'bg-slate-400' } else if (isPast) { label = 'Past' color = 'bg-gray-400' - } else if (isVirtual) { - label = 'Virtual' - color = 'bg-secondary' } return { @@ -52,6 +56,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { isVirtual, isPast, isRegistrationClosed, + isRegistrationClosingSoon, } } @@ -79,7 +84,11 @@ export function EventTable({ events = [] }: { events: Event[] }) { // Get display location (city/venue) const getLocation = (event: Event) => { if (event.location?.isVirtual) { - return 'Online' + return ( + + Virtual + + ) } const { placeName, city, state } = event.location || {} if (placeName) return placeName @@ -190,7 +199,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { {displayedEvents.map((event) => { const { date, time } = formatDateTime(event.startDate) const status = getStatus(event) - const { isVirtual, isPast, isRegistrationClosed } = status + const { isVirtual, isPast, isRegistrationClosed, isRegistrationClosingSoon } = status const isExpanded = expandedRows.has(String(event.id)) return ( @@ -242,15 +251,22 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Register button */} {event.registrationUrl && !isPast && !isRegistrationClosed ? ( - - Register - - + <> + + Register + + + {isRegistrationClosingSoon && ( + + ⚠️ Closing Soon + + )} + ) : isPast || isRegistrationClosed ? ( ) : ( @@ -280,22 +280,13 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Expanded row for details on smaller screens */} {isExpanded && ( - - -
- {/* Location */} -
-

Location

-

{getLocation(event)}

- {getAddress(event) && ( -

{getAddress(event)}

- )} -
- - {/* Status badge */} -
-
-

Status

+ + +
+
+ {/* Status badge */} +
+

Status

@@ -303,11 +294,31 @@ export function EventTable({ events = [] }: { events: Event[] }) {
- {isVirtual && ( -
-

Virtual

-

Yes

-
+ {/* Deadline date */} +
+ {event.registrationDeadline + ? (() => { + const { date, time } = formatDateTime(event.registrationDeadline) + return ( +
+

+ Registration deadline +

+
{date}
+
{time}
+
+ ) + })() + : null} +
+
+ + {/* Location */} +
+

Location

+

{getLocation(event)}

+ {getAddress(event) && ( +

{getAddress(event)}

)}
From c06499c1b74687ebcb523bca3a72f5c01398cf62 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 21:46:31 -0800 Subject: [PATCH 11/48] Fix seed file --- src/endpoints/seed/events.ts | 2 +- src/payload-types.ts | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/endpoints/seed/events.ts b/src/endpoints/seed/events.ts index cb36871b..b42f0ed6 100644 --- a/src/endpoints/seed/events.ts +++ b/src/endpoints/seed/events.ts @@ -102,7 +102,7 @@ export const getEventsData = ( featuredImage: featuredImage?.id, thumbnailImage: thumbnailImage?.id, registrationUrl: 'https://example.com/register/snowmobile-safety', - registrationDeadline: futureDate(1, 15, 23), + registrationDeadline: futureDate(1, 1, 23), capacity: 16, cost: 200, skillRating: '1', diff --git a/src/payload-types.ts b/src/payload-types.ts index 764a2f7b..d7fb88cf 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -978,14 +978,7 @@ export interface EventTableBlock { * Optionally select event types to filter events. */ filterByEventTypes?: - | ( - | 'events-by-ac' - | 'awareness' - | 'workshop' - | 'field-class-by-ac' - | 'course-by-external-provider' - | 'volunteer' - )[] + | ('events-by-ac' | 'awareness' | 'workshop' | 'field-class-by-ac' | 'volunteer' | 'events-by-others')[] | null; /** * Maximum number of events that will be displayed. Must be an integer. From 17b4292efbc681a92609ff13f68dd25ca57a2e6e Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 22:53:41 -0800 Subject: [PATCH 12/48] Move `validateMaxEvents` to reuse --- src/blocks/EventList/config.ts | 2 +- .../hooks/validateMaxEvents.ts | 0 src/blocks/EventTable/config.ts | 2 +- src/blocks/EventTable/hooks/validateMaxEvents.ts | 15 --------------- 4 files changed, 2 insertions(+), 17 deletions(-) rename src/blocks/{EventList => EventQuery}/hooks/validateMaxEvents.ts (100%) delete mode 100644 src/blocks/EventTable/hooks/validateMaxEvents.ts diff --git a/src/blocks/EventList/config.ts b/src/blocks/EventList/config.ts index 01dc6680..f5287676 100644 --- a/src/blocks/EventList/config.ts +++ b/src/blocks/EventList/config.ts @@ -9,9 +9,9 @@ import { } from '@payloadcms/richtext-lexical' import type { Block, Field } from 'payload' import { ButtonBlock } from '../Button/config' +import { validateMaxEvents } from '../EventQuery/hooks/validateMaxEvents' import { GenericEmbedLexical } from '../GenericEmbed/config' import { MediaBlockLexical } from '../MediaBlock/config' -import { validateMaxEvents } from './hooks/validateMaxEvents' const defaultStylingFields: Field[] = [ { name: 'heading', type: 'text' }, diff --git a/src/blocks/EventList/hooks/validateMaxEvents.ts b/src/blocks/EventQuery/hooks/validateMaxEvents.ts similarity index 100% rename from src/blocks/EventList/hooks/validateMaxEvents.ts rename to src/blocks/EventQuery/hooks/validateMaxEvents.ts diff --git a/src/blocks/EventTable/config.ts b/src/blocks/EventTable/config.ts index 769b742d..778f712d 100644 --- a/src/blocks/EventTable/config.ts +++ b/src/blocks/EventTable/config.ts @@ -8,9 +8,9 @@ import { } from '@payloadcms/richtext-lexical' import type { Block, Field, FilterOptionsProps } from 'payload' import { ButtonBlock } from '../Button/config' +import { validateMaxEvents } from '../EventQuery/hooks/validateMaxEvents' import { GenericEmbedLexical } from '../GenericEmbed/config' import { MediaBlockLexical } from '../MediaBlock/config' -import { validateMaxEvents } from './hooks/validateMaxEvents' const defaultStylingFields: Field[] = [ { name: 'heading', type: 'text' }, diff --git a/src/blocks/EventTable/hooks/validateMaxEvents.ts b/src/blocks/EventTable/hooks/validateMaxEvents.ts deleted file mode 100644 index 0f4ac8fb..00000000 --- a/src/blocks/EventTable/hooks/validateMaxEvents.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { FieldHook } from 'payload' - -export const validateMaxEvents: FieldHook = ({ value }) => { - if (value === undefined || value === null) { - return value - } - - const numValue = typeof value === 'string' ? parseFloat(value) : value - - if (!Number.isInteger(numValue)) { - return Math.floor(numValue) - } - - return numValue -} From 533e384be17726c171b97af4a7a086a1e9a6b6ac Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 22:54:59 -0800 Subject: [PATCH 13/48] Move `QueriedEventsComponent` to reuse --- src/blocks/EventList/config.ts | 2 +- .../fields/QueriedEventsComponent.tsx | 0 src/blocks/EventTable/config.ts | 2 +- .../fields/QueriedEventsComponent.tsx | 147 ------------------ 4 files changed, 2 insertions(+), 149 deletions(-) rename src/blocks/{EventList => EventQuery}/fields/QueriedEventsComponent.tsx (100%) delete mode 100644 src/blocks/EventTable/fields/QueriedEventsComponent.tsx diff --git a/src/blocks/EventList/config.ts b/src/blocks/EventList/config.ts index f5287676..25d4d362 100644 --- a/src/blocks/EventList/config.ts +++ b/src/blocks/EventList/config.ts @@ -125,7 +125,7 @@ const dynamicEventRelatedFields: Field[] = [ admin: { readOnly: true, components: { - Field: '@/blocks/EventList/fields/QueriedEventsComponent#QueriedEventsComponent', + Field: '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent', }, }, }, diff --git a/src/blocks/EventList/fields/QueriedEventsComponent.tsx b/src/blocks/EventQuery/fields/QueriedEventsComponent.tsx similarity index 100% rename from src/blocks/EventList/fields/QueriedEventsComponent.tsx rename to src/blocks/EventQuery/fields/QueriedEventsComponent.tsx diff --git a/src/blocks/EventTable/config.ts b/src/blocks/EventTable/config.ts index 778f712d..b4271af6 100644 --- a/src/blocks/EventTable/config.ts +++ b/src/blocks/EventTable/config.ts @@ -99,7 +99,7 @@ const dynamicEventRelatedFields: Field[] = [ admin: { readOnly: true, components: { - Field: '@/blocks/EventTable/fields/QueriedEventsComponent#QueriedEventsComponent', + Field: '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent', }, }, }, diff --git a/src/blocks/EventTable/fields/QueriedEventsComponent.tsx b/src/blocks/EventTable/fields/QueriedEventsComponent.tsx deleted file mode 100644 index 0d47f286..00000000 --- a/src/blocks/EventTable/fields/QueriedEventsComponent.tsx +++ /dev/null @@ -1,147 +0,0 @@ -'use client' - -import { Event } from '@/payload-types' -import { useTenantSelection } from '@/providers/TenantSelectionProvider/index.client' -import { FieldDescription, SelectInput, useField, useForm, useFormFields } from '@payloadcms/ui' -import { OptionObject } from 'payload' -import { useEffect, useState } from 'react' - -type QueriedEventsComponentProps = { - path: string - field?: { - label?: string - } -} - -export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentProps) => { - const label = field?.label || 'Queried Events' - const parentPathParts = path.split('.').slice(0, -1) - - const { value, setValue } = useField({ path }) - const [isLoading, setIsLoading] = useState(false) - const [fetchedEvents, setFetchedEvents] = useState([]) - const [selectOptions, setSelectOptions] = useState([]) - const { setDisabled } = useForm() - - const filterByEventTypes = useFormFields( - ([fields]) => fields[parentPathParts.concat(['filterByEventTypes']).join('.')]?.value, - ) - - const sortBy = useFormFields( - ([fields]) => fields[parentPathParts.concat(['sortBy']).join('.')]?.value, - ) - const maxEvents = useFormFields( - ([fields]) => fields[parentPathParts.concat(['maxEvents']).join('.')]?.value, - ) - const showUpcomingOnly = useFormFields( - ([fields]) => fields[parentPathParts.concat(['showUpcomingOnly']).join('.')]?.value, - ) - const { selectedTenantID: tenant } = useTenantSelection() - - useEffect(() => { - if (!tenant) { - return - } - - const fetchEvents = async () => { - setIsLoading(true) - setDisabled(true) - - try { - const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id - if (!tenantId) return - - const params = new URLSearchParams({ - limit: String(maxEvents || 4), - depth: '1', - 'where[tenant][equals]': String(tenantId), - }) - - if (sortBy) { - params.append('sort', String(sortBy)) - } - - if (showUpcomingOnly) { - params.append('where[startDate][greater_than]', new Date().toISOString()) - } - - if ( - filterByEventTypes && - Array.isArray(filterByEventTypes) && - filterByEventTypes.length > 0 - ) { - const typeIds = filterByEventTypes.filter(Boolean) - - if (typeIds.length > 0) { - params.append('where[type][in]', typeIds.join(',')) - } - } - - const response = await fetch(`/api/events?${params.toString()}`) - if (!response.ok) { - throw new Error('Failed to fetch events') - } - - const data = await response.json() - const events = data.docs || [] - setFetchedEvents(events) - - const options: OptionObject[] = events.map((event: Event) => ({ - label: event.title, - value: String(event.id), - })) - setSelectOptions(options) - - const currentEventIds = (value || []) - .map((event) => (typeof event === 'number' ? event : event.id)) - .sort() - const newEventIds = events.map((event: Event) => event.id).sort() - - if (JSON.stringify(currentEventIds) !== JSON.stringify(newEventIds)) { - setValue(events) - } - } catch (error) { - console.error('Error fetching events for EventTable block:', error) - setFetchedEvents([]) - setSelectOptions([]) - setValue([]) - } finally { - setIsLoading(false) - setDisabled(false) - } - } - - fetchEvents() - }, [ - filterByEventTypes, - sortBy, - maxEvents, - showUpcomingOnly, - tenant, - setValue, - value, - setDisabled, - ]) - - const currentValue = fetchedEvents.map((event) => String(event.id)) - - return ( -
- - -
- ) -} From 1cc78b1f3ec4bcd9ad020c2d79f9e888a2a2cea4 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 23:31:00 -0800 Subject: [PATCH 14/48] Consolidate EventList and EventTable configs --- src/app/(payload)/admin/importMap.js | 9 +- src/blocks/EventList/config.ts | 180 ++++----------------------- src/blocks/EventQuery/config.ts | 144 +++++++++++++++++++++ src/blocks/EventTable/config.ts | 139 +-------------------- src/payload-types.ts | 13 +- 5 files changed, 185 insertions(+), 300 deletions(-) create mode 100644 src/blocks/EventQuery/config.ts diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 7e85e6e1..d0a1df8e 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,7 +1,6 @@ import { QueriedPostsComponent as QueriedPostsComponent_a2ad38d8499118f1bebc7752c0fff51e } from '@/blocks/BlogList/fields/QueriedPostsComponent' import { DefaultColumnAdder as DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01 } from '@/blocks/Content/components/DefaultColumnAdder' -import { QueriedEventsComponent as QueriedEventsComponent_65bd30cc675f775ebce6af07a79e525c } from '@/blocks/EventList/fields/QueriedEventsComponent' -import { QueriedEventsComponent as QueriedEventsComponent_f2dcb9815766a4294d50909b2f64da85 } from '@/blocks/EventTable/fields/QueriedEventsComponent' +import { QueriedEventsComponent as QueriedEventsComponent_21a7403c1e15ec396d69bd72be28641d } from '@/blocks/EventQuery/fields/QueriedEventsComponent' import { SponsorsLayoutDescription as SponsorsLayoutDescription_6f00823041b5b0999b9929fb565110de } from '@/blocks/SponsorsBlock/components/SponsorsLayoutDescription' import { CourseTypeField as CourseTypeField_348fff62462d32a00f93a0ac5be86e99 } from '@/collections/Courses/components/CourseTypeField' import { DuplicatePageFor as DuplicatePageFor_8f1d8961a356bec6784e5c591c016925 } from '@/collections/Pages/components/DuplicatePageFor' @@ -105,10 +104,8 @@ export const importMap = { '@/components/ColumnLayoutPicker#default': default_923dc5ccc0b72de4298251644cbfe39e, '@/blocks/Content/components/DefaultColumnAdder#DefaultColumnAdder': DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01, - '@/blocks/EventList/fields/QueriedEventsComponent#QueriedEventsComponent': - QueriedEventsComponent_65bd30cc675f775ebce6af07a79e525c, - '@/blocks/EventTable/fields/QueriedEventsComponent#QueriedEventsComponent': - QueriedEventsComponent_f2dcb9815766a4294d50909b2f64da85, + '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent': + QueriedEventsComponent_21a7403c1e15ec396d69bd72be28641d, '@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, '@payloadcms/plugin-seo/client#MetaImageComponent': diff --git a/src/blocks/EventList/config.ts b/src/blocks/EventList/config.ts index 25d4d362..0fa587fb 100644 --- a/src/blocks/EventList/config.ts +++ b/src/blocks/EventList/config.ts @@ -1,160 +1,26 @@ -import { eventTypesData } from '@/collections/Events/constants' import colorPickerField from '@/fields/color' -import { getTenantFilter } from '@/utilities/collectionFilters' -import { - BlocksFeature, - HorizontalRuleFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' import type { Block, Field } from 'payload' -import { ButtonBlock } from '../Button/config' -import { validateMaxEvents } from '../EventQuery/hooks/validateMaxEvents' -import { GenericEmbedLexical } from '../GenericEmbed/config' -import { MediaBlockLexical } from '../MediaBlock/config' - -const defaultStylingFields: Field[] = [ - { name: 'heading', type: 'text' }, - { - name: 'belowHeadingContent', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - BlocksFeature({ - blocks: [ButtonBlock, MediaBlockLexical, GenericEmbedLexical], - }), - HorizontalRuleFeature(), - InlineToolbarFeature(), - ] - }, - }), - label: 'Content Below Heading', - admin: { - description: 'Optional content to display below the heading and above the event list.', - }, - }, - colorPickerField('Background color'), - { - type: 'radio', - name: 'eventOptions', - label: 'How do you want to choose your events?', - defaultValue: 'dynamic', - required: true, - options: [ - { - label: 'Do it for me', - value: 'dynamic', - }, - { - label: 'Let me choose', - value: 'static', - }, - ], - }, -] - -const dynamicEventRelatedFields: Field[] = [ - { - name: 'dynamicOptions', - type: 'group', - admin: { - condition: (_, siblingData) => siblingData?.eventOptions === 'dynamic', - }, - fields: [ - { - name: 'sortBy', - type: 'select', - defaultValue: 'startDate', - options: [ - { label: 'Start Date (Earliest First)', value: 'startDate' }, - { label: 'Start Date (Latest First)', value: '-startDate' }, - { label: 'Registration Deadline (Earliest First)', value: 'registrationDeadline' }, - { label: 'Registration Deadline (Latest First)', value: '-registrationDeadline' }, - ], - required: true, - admin: { - description: 'Select how the list of events will be sorted.', - }, - }, - { - name: 'filterByEventTypes', - type: 'select', - dbName: 'filterByEventTypes', - options: eventTypesData.map((type) => ({ - label: type.label, - value: type.value, - })), - hasMany: true, - label: 'Filter by Event Type(s)', - admin: { - description: 'Optionally select event types to filter events.', - }, - }, - { - name: 'showUpcomingOnly', - type: 'checkbox', - defaultValue: true, - label: 'Show Upcoming Events Only', - admin: { - description: 'Only display events that have not yet occurred.', - }, - }, - { - name: 'maxEvents', - type: 'number', - label: 'Max Events Displayed', - min: 1, - max: 20, - defaultValue: 4, - admin: { - description: 'Maximum number of events that will be displayed. Must be an integer.', - step: 1, - }, - hooks: { - beforeValidate: [validateMaxEvents], - }, - }, - { - name: 'queriedEvents', - type: 'relationship', - label: 'Preview Events Order', - relationTo: 'events', - hasMany: true, - admin: { - readOnly: true, - components: { - Field: '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent', - }, - }, - }, - ], - }, -] +import { + defaultStylingFields, + dynamicEventRelatedFields, + staticEventRelatedFields, +} from '../EventQuery/config' -const staticEventRelatedFields: Field[] = [ - { - name: 'staticOptions', - type: 'group', - admin: { - condition: (_, siblingData) => siblingData?.eventOptions === 'static', - }, - fields: [ - { - name: 'staticEvents', - type: 'relationship', - label: 'Choose events', - relationTo: 'events', - hasMany: true, - admin: { - description: 'Choose new event from dropdown and/or drag and drop to change order', - }, - filterOptions: getTenantFilter, - }, - ], +const sortByField = (): Field => ({ + name: 'sortBy', + type: 'select', + defaultValue: 'startDate', + options: [ + { label: 'Start Date (Earliest First)', value: 'startDate' }, + { label: 'Start Date (Latest First)', value: '-startDate' }, + { label: 'Registration Deadline (Earliest First)', value: 'registrationDeadline' }, + { label: 'Registration Deadline (Latest First)', value: '-registrationDeadline' }, + ], + required: true, + admin: { + description: 'Select how the list of events will be sorted.', }, -] +}) const eventListBlockWithFields = (fields: Field[]): Block => ({ slug: 'eventList', @@ -164,13 +30,13 @@ const eventListBlockWithFields = (fields: Field[]): Block => ({ }) export const EventListBlock = eventListBlockWithFields([ - ...defaultStylingFields, - ...dynamicEventRelatedFields, + ...defaultStylingFields([colorPickerField('Background color')]), + ...dynamicEventRelatedFields([sortByField()]), ...staticEventRelatedFields, ]) export const EventListBlockLexical = eventListBlockWithFields([ - ...defaultStylingFields, + ...defaultStylingFields([colorPickerField('Background color')]), { name: 'wrapInContainer', admin: { @@ -180,6 +46,6 @@ export const EventListBlockLexical = eventListBlockWithFields([ type: 'checkbox', defaultValue: false, }, - ...dynamicEventRelatedFields, + ...dynamicEventRelatedFields([sortByField()]), ...staticEventRelatedFields, ]) diff --git a/src/blocks/EventQuery/config.ts b/src/blocks/EventQuery/config.ts new file mode 100644 index 00000000..5c8eaaf4 --- /dev/null +++ b/src/blocks/EventQuery/config.ts @@ -0,0 +1,144 @@ +import { eventTypesData } from '@/collections/Events/constants' +import { getTenantFilter } from '@/utilities/collectionFilters' +import { + BlocksFeature, + HorizontalRuleFeature, + InlineToolbarFeature, + lexicalEditor, +} from '@payloadcms/richtext-lexical' +import type { Field, FilterOptionsProps } from 'payload' +import { ButtonBlock } from '../Button/config' +import { GenericEmbedLexical } from '../GenericEmbed/config' +import { MediaBlockLexical } from '../MediaBlock/config' +import { validateMaxEvents } from './hooks/validateMaxEvents' + +export const defaultStylingFields = (additionalFilters?: Field[]): Field[] => [ + ...(additionalFilters ?? []), + { name: 'heading', type: 'text' }, + { + name: 'belowHeadingContent', + type: 'richText', + editor: lexicalEditor({ + features: ({ rootFeatures }) => { + return [ + ...rootFeatures, + BlocksFeature({ + blocks: [ButtonBlock, MediaBlockLexical, GenericEmbedLexical], + }), + HorizontalRuleFeature(), + InlineToolbarFeature(), + ] + }, + }), + label: 'Content Below Heading', + admin: { + description: 'Optional content to display below the heading and above the event content.', + }, + }, + { + type: 'radio', + name: 'eventOptions', + label: 'How do you want to choose your events?', + defaultValue: 'dynamic', + required: true, + options: [ + { + label: 'Do it for me', + value: 'dynamic', + }, + { + label: 'Let me choose', + value: 'static', + }, + ], + }, +] + +export const dynamicEventRelatedFields = (additionalFilters?: Field[]): Field[] => [ + { + name: 'dynamicOptions', + type: 'group', + admin: { + condition: (_, siblingData) => siblingData?.eventOptions === 'dynamic', + }, + fields: [ + ...(additionalFilters ?? []), + { + name: 'filterByEventTypes', + type: 'select', + dbName: 'filterByEventTypes', + options: eventTypesData.map((type) => ({ + label: type.label, + value: type.value, + })), + hasMany: true, + label: 'Filter by Event Type(s)', + admin: { + description: 'Optionally select event types to filter events.', + }, + }, + { + name: 'showUpcomingOnly', + type: 'checkbox', + defaultValue: true, + label: 'Show Upcoming Events Only', + admin: { + description: 'Only display events that have not yet occurred.', + }, + }, + { + name: 'maxEvents', + type: 'number', + label: 'Max Events Displayed', + min: 1, + max: 20, + defaultValue: 4, + admin: { + description: 'Maximum number of events that will be displayed. Must be an integer.', + step: 1, + }, + hooks: { + beforeValidate: [validateMaxEvents], + }, + }, + { + name: 'queriedEvents', + type: 'relationship', + label: 'Preview Events Order', + relationTo: 'events', + hasMany: true, + admin: { + readOnly: true, + components: { + Field: '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent', + }, + }, + }, + ], + }, +] + +export const staticEventRelatedFields: Field[] = [ + { + name: 'staticOptions', + type: 'group', + admin: { + condition: (_, siblingData) => siblingData?.eventOptions === 'static', + }, + fields: [ + { + name: 'staticEvents', + type: 'relationship', + label: 'Choose events', + relationTo: 'events', + hasMany: true, + admin: { + description: 'Choose new event from dropdown and/or drag and drop to change order', + }, + filterOptions: (props: FilterOptionsProps) => ({ + and: [getTenantFilter(props)], + }), + }, + ], + }, +] diff --git a/src/blocks/EventTable/config.ts b/src/blocks/EventTable/config.ts index b4271af6..c3efc272 100644 --- a/src/blocks/EventTable/config.ts +++ b/src/blocks/EventTable/config.ts @@ -1,140 +1,13 @@ -import { eventTypesData } from '@/collections/Events/constants' -import { getTenantFilter } from '@/utilities/collectionFilters' +import type { Block } from 'payload' import { - BlocksFeature, - HorizontalRuleFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' -import type { Block, Field, FilterOptionsProps } from 'payload' -import { ButtonBlock } from '../Button/config' -import { validateMaxEvents } from '../EventQuery/hooks/validateMaxEvents' -import { GenericEmbedLexical } from '../GenericEmbed/config' -import { MediaBlockLexical } from '../MediaBlock/config' - -const defaultStylingFields: Field[] = [ - { name: 'heading', type: 'text' }, - { - name: 'belowHeadingContent', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - BlocksFeature({ - blocks: [ButtonBlock, MediaBlockLexical, GenericEmbedLexical], - }), - HorizontalRuleFeature(), - InlineToolbarFeature(), - ] - }, - }), - label: 'Content Below Heading', - admin: { - description: 'Optional content to display below the heading and above the event table.', - }, - }, - { - type: 'radio', - name: 'eventOptions', - label: 'How do you want to choose your events?', - defaultValue: 'dynamic', - required: true, - options: [ - { - label: 'Do it for me', - value: 'dynamic', - }, - { - label: 'Let me choose', - value: 'static', - }, - ], - }, -] - -const dynamicEventRelatedFields: Field[] = [ - { - name: 'dynamicOptions', - type: 'group', - admin: { - condition: (_, siblingData) => siblingData?.eventOptions === 'dynamic', - }, - fields: [ - { - name: 'filterByEventTypes', - type: 'select', - dbName: 'filterByEventTypes', - options: eventTypesData.map((type) => ({ - label: type.label, - value: type.value, - })), - hasMany: true, - label: 'Filter by Event Type(s)', - admin: { - description: 'Optionally select event types to filter events.', - }, - }, - { - name: 'maxEvents', - type: 'number', - label: 'Max Events Displayed', - min: 1, - max: 20, - defaultValue: 4, - admin: { - description: 'Maximum number of events that will be displayed. Must be an integer.', - step: 1, - }, - hooks: { - beforeValidate: [validateMaxEvents], - }, - }, - { - name: 'queriedEvents', - type: 'relationship', - label: 'Preview Events Order', - relationTo: 'events', - hasMany: true, - admin: { - readOnly: true, - components: { - Field: '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent', - }, - }, - }, - ], - }, -] - -const staticEventRelatedFields: Field[] = [ - { - name: 'staticOptions', - type: 'group', - admin: { - condition: (_, siblingData) => siblingData?.eventOptions === 'static', - }, - fields: [ - { - name: 'staticEvents', - type: 'relationship', - label: 'Choose events', - relationTo: 'events', - hasMany: true, - admin: { - description: 'Choose new event from dropdown and/or drag and drop to change order', - }, - filterOptions: (props: FilterOptionsProps) => ({ - and: [getTenantFilter(props)], - }), - }, - ], - }, -] + defaultStylingFields, + dynamicEventRelatedFields, + staticEventRelatedFields, +} from '../EventQuery/config' export const EventTableBlock: Block = { slug: 'eventTable', interfaceName: 'EventTableBlock', imageURL: '/thumbnail/EventTableThumbnail.jpg', - fields: [...defaultStylingFields, ...dynamicEventRelatedFields, ...staticEventRelatedFields], + fields: [...defaultStylingFields(), ...dynamicEventRelatedFields(), ...staticEventRelatedFields], } diff --git a/src/payload-types.ts b/src/payload-types.ts index d7fb88cf..ff339fd2 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -672,9 +672,10 @@ export interface Document { * via the `definition` "EventListBlock". */ export interface EventListBlock { + backgroundColor: string; heading?: string | null; /** - * Optional content to display below the heading and above the event list. + * Optional content to display below the heading and above the event content. */ belowHeadingContent?: { root: { @@ -691,7 +692,6 @@ export interface EventListBlock { }; [k: string]: unknown; } | null; - backgroundColor: string; eventOptions: 'dynamic' | 'static'; /** * Checking this will render the block with additional padding around it and using the background color you have selected. @@ -955,7 +955,7 @@ export interface EventTag { export interface EventTableBlock { heading?: string | null; /** - * Optional content to display below the heading and above the event table. + * Optional content to display below the heading and above the event content. */ belowHeadingContent?: { root: { @@ -980,6 +980,10 @@ export interface EventTableBlock { filterByEventTypes?: | ('events-by-ac' | 'awareness' | 'workshop' | 'field-class-by-ac' | 'volunteer' | 'events-by-others')[] | null; + /** + * Only display events that have not yet occurred. + */ + showUpcomingOnly?: boolean | null; /** * Maximum number of events that will be displayed. Must be an integer. */ @@ -2889,9 +2893,9 @@ export interface DocumentBlockSelect { * via the `definition` "EventListBlock_select". */ export interface EventListBlockSelect { + backgroundColor?: T; heading?: T; belowHeadingContent?: T; - backgroundColor?: T; eventOptions?: T; dynamicOptions?: | T @@ -3147,6 +3151,7 @@ export interface EventTableBlockSelect { | T | { filterByEventTypes?: T; + showUpcomingOnly?: T; maxEvents?: T; queriedEvents?: T; }; From 5d9006a7510224a8d5b2cee7fa49ea923e99ec73 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 10 Nov 2025 23:50:39 -0800 Subject: [PATCH 15/48] Fix mobile expanded table --- src/components/EventsTable/index.tsx | 37 +++++++++++++--------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index b7f0c788..a030f834 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -54,7 +54,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { const formatDateTime = (dateString: string) => { const date = new Date(dateString) return { - date: format(date, 'MMM d, yyyy'), // "Nov 10, 2025" + date: format(date, 'MMM d, yy'), time: format(date, 'h:mm a'), } } @@ -283,15 +283,19 @@ export function EventTable({ events = [] }: { events: Event[] }) {
-
- {/* Status badge */} -
+ {/* Location */} +
+

Location

+

{getLocation(event)}

+ {getAddress(event) && ( +

{getAddress(event)}

+ )} +
+
+ {/* Status */} +

Status

- - {status.label} - + {status.label}
{/* Deadline date */} @@ -304,23 +308,16 @@ export function EventTable({ events = [] }: { events: Event[] }) {

Registration deadline

-
{date}
-
{time}
+
+ {date} + @ {time} +
) })() : null}
- - {/* Location */} -
-

Location

-

{getLocation(event)}

- {getAddress(event) && ( -

{getAddress(event)}

- )} -
From 4a00681568215284be3c5fb07d53c097a24b6333 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Tue, 11 Nov 2025 00:00:10 -0800 Subject: [PATCH 16/48] Force dynamic for event group page --- src/app/(frontend)/[center]/events/g/[slug]/page.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx index 67a7a636..c01f6ff9 100644 --- a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx +++ b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx @@ -9,6 +9,8 @@ import { generateMetaForEvent } from '@/utilities/generateMeta' import { cn } from '@/utilities/ui' import { Metadata, ResolvedMetadata } from 'next' +export const dynamic = 'force-dynamic' + export async function generateStaticParams() { const payload = await getPayload({ config: configPromise }) const events = await payload.find({ From 3fbf95999e1a9462f6d52eea656852fbb1c0327f Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Tue, 11 Nov 2025 00:29:37 -0800 Subject: [PATCH 17/48] Add EventTable to other richText fields and add thumbnail --- public/thumbnail/EventTableThumbnail.jpg | Bin 0 -> 16338 bytes src/collections/Events/index.ts | 2 + src/collections/HomePages/index.tsx | 2 + src/collections/Posts/index.ts | 2 + src/payload-types.ts | 50 ++++++++++++----------- 5 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 public/thumbnail/EventTableThumbnail.jpg diff --git a/public/thumbnail/EventTableThumbnail.jpg b/public/thumbnail/EventTableThumbnail.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dda903a0026d7fab593f016affcc1f0bbe40e505 GIT binary patch literal 16338 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 r=d_KNtAsp+N = { BlogListBlockLexical, DocumentBlock, EventListBlockLexical, + EventTableBlock, GenericEmbedLexical, HeaderBlock, MediaBlockLexical, diff --git a/src/payload-types.ts b/src/payload-types.ts index ff339fd2..4e127a59 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -251,6 +251,7 @@ export interface HomePage { | DocumentBlock | EventListBlock | SingleEventBlock + | EventTableBlock | FormBlock | GenericEmbedBlock | HeaderBlock @@ -2790,6 +2791,7 @@ export interface HomePagesSelect { documentBlock?: T | DocumentBlockSelect; eventList?: T | EventListBlockSelect; singleEvent?: T | SingleEventBlockSelect; + eventTable?: T | EventTableBlockSelect; formBlock?: T | FormBlockSelect; genericEmbed?: T | GenericEmbedBlockSelect; headerBlock?: T | HeaderBlockSelect; @@ -2924,6 +2926,30 @@ export interface SingleEventBlockSelect { id?: T; blockName?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "EventTableBlock_select". + */ +export interface EventTableBlockSelect { + heading?: T; + belowHeadingContent?: T; + eventOptions?: T; + dynamicOptions?: + | T + | { + filterByEventTypes?: T; + showUpcomingOnly?: T; + maxEvents?: T; + queriedEvents?: T; + }; + staticOptions?: + | T + | { + staticEvents?: T; + }; + id?: T; + blockName?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "FormBlock_select". @@ -3139,30 +3165,6 @@ export interface PagesSelect { createdAt?: T; _status?: T; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "EventTableBlock_select". - */ -export interface EventTableBlockSelect { - heading?: T; - belowHeadingContent?: T; - eventOptions?: T; - dynamicOptions?: - | T - | { - filterByEventTypes?: T; - showUpcomingOnly?: T; - maxEvents?: T; - queriedEvents?: T; - }; - staticOptions?: - | T - | { - staticEvents?: T; - }; - id?: T; - blockName?: T; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "posts_select". From 8d193606ad30ea6c05c86bd154e5f2748298382d Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Tue, 11 Nov 2025 00:32:34 -0800 Subject: [PATCH 18/48] Fix container class --- src/blocks/EventTable/Component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 5151f3e1..c232933e 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -34,7 +34,7 @@ export const EventTableBlockComponent = async (args: EventTableComponentProps) = } return ( -
+
{heading && (
From 8f74070cfb1c72dba20452e65428ccb7b427bb57 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Tue, 11 Nov 2025 10:54:30 -0800 Subject: [PATCH 19/48] Add clickable title --- src/components/EventsTable/index.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index a030f834..2e2c2904 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -194,6 +194,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { const status = getStatus(event) const { isPast, isRegistrationClosed } = status const isExpanded = expandedRows.has(String(event.id)) + const eventUrl = `/events/${event.slug}` return ( @@ -219,7 +220,12 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Name */} - {event.title} + + + {event.title} + + {event.title} + {/* Status label */} @@ -299,7 +305,7 @@ export function EventTable({ events = [] }: { events: Event[] }) {
{/* Deadline date */} -
+
{event.registrationDeadline ? (() => { const { date, time } = formatDateTime(event.registrationDeadline) @@ -317,6 +323,12 @@ export function EventTable({ events = [] }: { events: Event[] }) { })() : null}
+
+ + Learn More + + +
From 1f63d5d7621865bbffde442d2f64c320c6565f2d Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Tue, 11 Nov 2025 11:13:37 -0800 Subject: [PATCH 20/48] Update event group page top --- src/app/(frontend)/[center]/events/g/[slug]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx index c01f6ff9..5a2dfc5d 100644 --- a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx +++ b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx @@ -66,8 +66,8 @@ export default async function EventGroup({ params: paramsPromise }: Args) { )}
-
-
+
+

{event.title}

{event.description}
From ed6e61559066202196a2934da55992fd4ba759c9 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 12 Nov 2025 11:04:52 -0800 Subject: [PATCH 21/48] Remove registration deadline --- src/components/EventsTable/index.tsx | 35 ---------------------------- 1 file changed, 35 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index 2e2c2904..d30d3448 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -182,9 +182,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { - - - @@ -246,19 +243,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { {event.cost === 0 ? 'Free' : `$${event.cost}`} - - {event.registrationDeadline - ? (() => { - const { date, time } = formatDateTime(event.registrationDeadline) - return ( -
-
{date}
-
{time}
-
- ) - })() - : null} -
{/* Register button */} @@ -304,25 +288,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { {status.label}
- {/* Deadline date */} -
- {event.registrationDeadline - ? (() => { - const { date, time } = formatDateTime(event.registrationDeadline) - return ( -
-

- Registration deadline -

-
- {date} - @ {time} -
-
- ) - })() - : null} -
Learn More From 085aeeeac01c6b87fe8f99b7a33c9a98c5947530 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 12 Nov 2025 11:06:20 -0800 Subject: [PATCH 22/48] Remove cost --- src/components/EventsTable/index.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index d30d3448..681251b1 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -179,9 +179,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { Location - - - @@ -240,9 +237,6 @@ export function EventTable({ events = [] }: { events: Event[] }) {
{getAddress(event)}
- - {event.cost === 0 ? 'Free' : `$${event.cost}`} - {/* Register button */} From fd41af4ec98130c61157cde2d553a10ff09afcaa Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 12 Nov 2025 11:12:09 -0800 Subject: [PATCH 23/48] Remove status column --- src/components/EventsTable/index.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index 681251b1..d4fcddfe 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -175,9 +175,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { - - - Location @@ -221,15 +218,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { {event.title} - {/* Status label */} - - - {status.label} - - - {/* Location */}
@@ -276,12 +264,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { )}
- {/* Status */} -
-

Status

- {status.label} -
-
Learn More From 70a11d4dae7ccad05fc66baa0ce28b7ce58c9ec0 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 12 Nov 2025 11:21:49 -0800 Subject: [PATCH 24/48] Make title look clickable and update location info --- src/components/EventsTable/index.tsx | 32 +++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index d4fcddfe..4102fe51 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -64,11 +64,15 @@ export function EventTable({ events = [] }: { events: Event[] }) { if (event.location?.isVirtual) { return '' } + + const eventAddress: string[] = [] const { address, city, state, zip } = event.location || {} - if (address) return address - if (city && state) return `${city}, ${state}` - if (zip) return zip - return 'TBA' + + if (address) eventAddress.push(address) + if (city && state) eventAddress.push(`${city}, ${state}`) + if (zip) eventAddress.push(zip) + + return eventAddress.length > 0 ? eventAddress.join(', ') : 'TBA' } // Get display location (city/venue) @@ -212,7 +216,11 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Name */} - + {event.title} {event.title} @@ -227,7 +235,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Register button */} - + {event.registrationUrl && !isPast && !isRegistrationClosed ? ( <> {getAddress(event)}

)}
-
-
- - Learn More - - -
+
+ + Learn More + +
From 01b12e21820a5a3c8e38b66bb52f9ff59e6c363d Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 12 Nov 2025 11:53:10 -0800 Subject: [PATCH 25/48] Update registration button to be in accordion on small screens --- src/components/EventsTable/index.tsx | 48 +++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index 4102fe51..dbfda518 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -11,7 +11,14 @@ import { } from '@/components/ui/table' import type { Event } from '@/payload-types' import { format } from 'date-fns' -import { ChevronDown, ChevronRight, ChevronsUpDown, ChevronUp, ExternalLink } from 'lucide-react' +import { + ChevronDown, + ChevronRight, + ChevronsUpDown, + ChevronUp, + ExternalLink, + MapPin, +} from 'lucide-react' import { Fragment, useMemo, useState } from 'react' import { CMSLink } from '../Link' @@ -180,7 +187,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { Location - + @@ -235,7 +242,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Register button */} - + {event.registrationUrl && !isPast && !isRegistrationClosed ? ( <> -
+
{/* Location */} -
-

Location

+
+
+ + +

Location

+

{getLocation(event)}

{getAddress(event) && (

{getAddress(event)}

)}
-
+
+
+ {event.registrationUrl && !isPast && !isRegistrationClosed ? ( + <> + + Register + + + + ) : isPast || isRegistrationClosed ? ( + + ) : ( + + )} +
+ Learn More -
From e92b143d86fafaae0a23c66dbd2c266ad2f2881e Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 12 Nov 2025 11:57:43 -0800 Subject: [PATCH 26/48] Make breakpoint for button smaller --- src/components/EventsTable/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index dbfda518..eb546c0a 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -187,7 +187,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { Location - + @@ -242,7 +242,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { {/* Register button */} - + {event.registrationUrl && !isPast && !isRegistrationClosed ? ( <>
-
+
{event.registrationUrl && !isPast && !isRegistrationClosed ? ( <> Date: Wed, 12 Nov 2025 12:11:36 -0800 Subject: [PATCH 27/48] Fix z-index on group page --- src/app/(frontend)/[center]/events/g/[slug]/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx index 5a2dfc5d..ba745db4 100644 --- a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx +++ b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx @@ -60,11 +60,11 @@ export default async function EventGroup({ params: paramsPromise }: Args) { {event.featuredImage && ( )} -
+
From ba8c226c05edd6e800260b02a50640000271774e Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 13 Nov 2025 11:50:32 -0800 Subject: [PATCH 28/48] Move event query --- src/blocks/EventList/config.ts | 4 ++-- src/blocks/EventTable/config.ts | 4 ++-- .../EventQuery}/QueriedEventsComponent.tsx | 0 src/{blocks => fields}/EventQuery/config.ts | 11 ++++++----- .../EventQuery/hooks/validateMaxEvents.ts | 0 5 files changed, 10 insertions(+), 9 deletions(-) rename src/{blocks/EventQuery/fields => fields/EventQuery}/QueriedEventsComponent.tsx (100%) rename src/{blocks => fields}/EventQuery/config.ts (92%) rename src/{blocks => fields}/EventQuery/hooks/validateMaxEvents.ts (100%) diff --git a/src/blocks/EventList/config.ts b/src/blocks/EventList/config.ts index 0fa587fb..6036cf8a 100644 --- a/src/blocks/EventList/config.ts +++ b/src/blocks/EventList/config.ts @@ -1,10 +1,10 @@ import colorPickerField from '@/fields/color' -import type { Block, Field } from 'payload' import { defaultStylingFields, dynamicEventRelatedFields, staticEventRelatedFields, -} from '../EventQuery/config' +} from '@/fields/EventQuery/config' +import type { Block, Field } from 'payload' const sortByField = (): Field => ({ name: 'sortBy', diff --git a/src/blocks/EventTable/config.ts b/src/blocks/EventTable/config.ts index c3efc272..5c7d7245 100644 --- a/src/blocks/EventTable/config.ts +++ b/src/blocks/EventTable/config.ts @@ -1,9 +1,9 @@ -import type { Block } from 'payload' import { defaultStylingFields, dynamicEventRelatedFields, staticEventRelatedFields, -} from '../EventQuery/config' +} from '@/fields/EventQuery/config' +import type { Block } from 'payload' export const EventTableBlock: Block = { slug: 'eventTable', diff --git a/src/blocks/EventQuery/fields/QueriedEventsComponent.tsx b/src/fields/EventQuery/QueriedEventsComponent.tsx similarity index 100% rename from src/blocks/EventQuery/fields/QueriedEventsComponent.tsx rename to src/fields/EventQuery/QueriedEventsComponent.tsx diff --git a/src/blocks/EventQuery/config.ts b/src/fields/EventQuery/config.ts similarity index 92% rename from src/blocks/EventQuery/config.ts rename to src/fields/EventQuery/config.ts index 5c8eaaf4..62ce9d1e 100644 --- a/src/blocks/EventQuery/config.ts +++ b/src/fields/EventQuery/config.ts @@ -1,3 +1,8 @@ +import type { Field, FilterOptionsProps } from 'payload' + +import { ButtonBlock } from '@/blocks/Button/config' +import { GenericEmbedLexical } from '@/blocks/GenericEmbed/config' +import { MediaBlockLexical } from '@/blocks/MediaBlock/config' import { eventTypesData } from '@/collections/Events/constants' import { getTenantFilter } from '@/utilities/collectionFilters' import { @@ -6,10 +11,6 @@ import { InlineToolbarFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' -import type { Field, FilterOptionsProps } from 'payload' -import { ButtonBlock } from '../Button/config' -import { GenericEmbedLexical } from '../GenericEmbed/config' -import { MediaBlockLexical } from '../MediaBlock/config' import { validateMaxEvents } from './hooks/validateMaxEvents' export const defaultStylingFields = (additionalFilters?: Field[]): Field[] => [ @@ -110,7 +111,7 @@ export const dynamicEventRelatedFields = (additionalFilters?: Field[]): Field[] admin: { readOnly: true, components: { - Field: '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent', + Field: '@/fields/EventQuery/QueriedEventsComponent#QueriedEventsComponent', }, }, }, diff --git a/src/blocks/EventQuery/hooks/validateMaxEvents.ts b/src/fields/EventQuery/hooks/validateMaxEvents.ts similarity index 100% rename from src/blocks/EventQuery/hooks/validateMaxEvents.ts rename to src/fields/EventQuery/hooks/validateMaxEvents.ts From 595876ed234c448ae1e2b50eca75855ceffabe12 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 13 Nov 2025 11:54:19 -0800 Subject: [PATCH 29/48] Add tenant filter to groups and tags --- src/collections/Events/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/collections/Events/index.ts b/src/collections/Events/index.ts index bf6ed0d2..77562e15 100644 --- a/src/collections/Events/index.ts +++ b/src/collections/Events/index.ts @@ -18,7 +18,7 @@ import { slugField } from '@/fields/slug' import { startAndEndDateField } from '@/fields/startAndEndDateField' import { tenantField } from '@/fields/tenantField' import { populatePublishedAt } from '@/hooks/populatePublishedAt' -import { getImageTypeFilter } from '@/utilities/collectionFilters' +import { getImageTypeFilter, getTenantFilter } from '@/utilities/collectionFilters' import { TIMEZONE_OPTIONS } from '@/utilities/timezones' import { MetaImageField } from '@payloadcms/plugin-seo/fields' import { @@ -244,6 +244,7 @@ export const Events: CollectionConfig = { admin: { position: 'sidebar', }, + filterOptions: getTenantFilter, }, { name: 'eventTags', @@ -253,6 +254,7 @@ export const Events: CollectionConfig = { admin: { position: 'sidebar', }, + filterOptions: getTenantFilter, }, modeOfTravelField(), tenantField(), From da4f8856c3a438be9512224b090eb74f75111d74 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 13 Nov 2025 11:58:26 -0800 Subject: [PATCH 30/48] Remove events group page --- .../[center]/events/g/[slug]/page.tsx | 131 ------------------ src/collections/EventGroups/index.ts | 86 ------------ 2 files changed, 217 deletions(-) delete mode 100644 src/app/(frontend)/[center]/events/g/[slug]/page.tsx diff --git a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx b/src/app/(frontend)/[center]/events/g/[slug]/page.tsx deleted file mode 100644 index ba745db4..00000000 --- a/src/app/(frontend)/[center]/events/g/[slug]/page.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import RichText from '@/components/RichText' -import configPromise from '@payload-config' -import { draftMode } from 'next/headers' -import { getPayload } from 'payload' - -import { LivePreviewListener } from '@/components/LivePreviewListener' -import { Media } from '@/components/Media' -import { generateMetaForEvent } from '@/utilities/generateMeta' -import { cn } from '@/utilities/ui' -import { Metadata, ResolvedMetadata } from 'next' - -export const dynamic = 'force-dynamic' - -export async function generateStaticParams() { - const payload = await getPayload({ config: configPromise }) - const events = await payload.find({ - collection: 'events', - limit: 1000, - pagination: false, - depth: 3, - select: { - tenant: true, - slug: true, - }, - }) - - const params: PathArgs[] = [] - for (const event of events.docs) { - if (typeof event.tenant === 'number') { - payload.logger.error(`got number for event tenant`) - continue - } - if (event.tenant) { - params.push({ center: event.tenant.slug, slug: event.slug }) - } - } - - return params -} - -type Args = { - params: Promise -} - -type PathArgs = { - center: string - slug: string -} - -export default async function EventGroup({ params: paramsPromise }: Args) { - const { isEnabled: draft } = await draftMode() - const { center, slug } = await paramsPromise - const event = await queryEventBySlug({ center: center, slug: slug }) - - return ( -
- {draft && } - -
- {event.featuredImage && ( - - )} -
-
-
-
-

{event.title}

-
-
{event.description}
-
-
- - {event.content && ( - - )} -
-
-
- ) -} - -export async function generateMetadata( - { params: paramsPromise }: Args, - parent: Promise, -): Promise { - const parentMeta = (await parent) as Metadata - const { center, slug = '' } = await paramsPromise - const event = await queryEventBySlug({ center: center, slug: slug }) - - return generateMetaForEvent({ center: center, doc: event, parentMeta }) -} - -const queryEventBySlug = async ({ center, slug }: { center: string; slug: string }) => { - const { isEnabled: draft } = await draftMode() - - const payload = await getPayload({ config: configPromise }) - - const result = await payload.find({ - collection: 'eventGroups', - draft, - limit: 1, - pagination: false, - populate: { - tenants: { - slug: true, - name: true, - customDomain: true, - }, - }, - where: { - and: [ - { - 'tenant.slug': { - equals: center, - }, - }, - { - slug: { - equals: slug, - }, - }, - ], - }, - }) - - return result.docs?.[0] || null -} diff --git a/src/collections/EventGroups/index.ts b/src/collections/EventGroups/index.ts index cbffd775..c8be35df 100644 --- a/src/collections/EventGroups/index.ts +++ b/src/collections/EventGroups/index.ts @@ -3,31 +3,8 @@ import { filterByTenant } from '@/access/filterByTenant' import { contentHashField } from '@/fields/contentHashField' import { slugField } from '@/fields/slug' import { tenantField } from '@/fields/tenantField' -import { getImageTypeFilter } from '@/utilities/collectionFilters' import { CollectionConfig } from 'payload' -import { MetaImageField } from '@payloadcms/plugin-seo/fields' -import { - BlocksFeature, - HorizontalRuleFeature, - InlineToolbarFeature, - lexicalEditor, -} from '@payloadcms/richtext-lexical' - -import { Banner } from '@/blocks/Banner/config' -import { BlogListBlockLexical } from '@/blocks/BlogList/config' -import { ButtonBlock } from '@/blocks/Button/config' -import { CalloutBlock } from '@/blocks/Callout/config' -import { DocumentBlock } from '@/blocks/DocumentBlock/config' -import { EventListBlockLexical } from '@/blocks/EventList/config' -import { EventTableBlock } from '@/blocks/EventTable/config' -import { GenericEmbedLexical } from '@/blocks/GenericEmbed/config' -import { HeaderBlock } from '@/blocks/Header/config' -import { MediaBlockLexical } from '@/blocks/MediaBlock/config' -import { SingleBlogPostBlockLexical } from '@/blocks/SingleBlogPost/config' -import { SingleEventBlockLexical } from '@/blocks/SingleEvent/config' -import { SponsorsBlock } from '@/blocks/SponsorsBlock/config' - export const EventGroups: CollectionConfig = { slug: 'eventGroups', access: accessByTenantRole('eventGroups'), @@ -52,70 +29,7 @@ export const EventGroups: CollectionConfig = { }, ], }, - { - type: 'group', - label: 'Landing Page Content', - admin: { - description: - "Create page content for this event's landing page. This landing page will only be displayed if there is not an External Event URL.", - }, - fields: [ - { - name: 'content', - label: '', - type: 'richText', - editor: lexicalEditor({ - features: ({ rootFeatures }) => { - return [ - ...rootFeatures, - BlocksFeature({ - blocks: [ - Banner, - BlogListBlockLexical, - ButtonBlock, - CalloutBlock, - DocumentBlock, - EventListBlockLexical, - EventTableBlock, - SingleEventBlockLexical, - GenericEmbedLexical, - HeaderBlock, - MediaBlockLexical, - SingleBlogPostBlockLexical, - SponsorsBlock, - ], - }), - HorizontalRuleFeature(), - InlineToolbarFeature(), - ] - }, - }), - required: true, - }, - ], - }, slugField(), - MetaImageField({ - hasGenerateFn: true, - relationTo: 'media', - overrides: { - admin: { - allowCreate: true, - position: 'sidebar', - }, - name: 'featuredImage', - label: 'Featured image', - }, - }), - { - name: 'thumbnailImage', - type: 'upload', - relationTo: 'media', - filterOptions: getImageTypeFilter, - admin: { - position: 'sidebar', - }, - }, contentHashField(), ], } From 4c53729e5faf4ffff17dfbbc529b81e1225c9abb Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 13 Nov 2025 11:59:00 -0800 Subject: [PATCH 31/48] Update import map --- src/app/(payload)/admin/importMap.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index d0a1df8e..a93becc5 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -1,6 +1,5 @@ import { QueriedPostsComponent as QueriedPostsComponent_a2ad38d8499118f1bebc7752c0fff51e } from '@/blocks/BlogList/fields/QueriedPostsComponent' import { DefaultColumnAdder as DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01 } from '@/blocks/Content/components/DefaultColumnAdder' -import { QueriedEventsComponent as QueriedEventsComponent_21a7403c1e15ec396d69bd72be28641d } from '@/blocks/EventQuery/fields/QueriedEventsComponent' import { SponsorsLayoutDescription as SponsorsLayoutDescription_6f00823041b5b0999b9929fb565110de } from '@/blocks/SponsorsBlock/components/SponsorsLayoutDescription' import { CourseTypeField as CourseTypeField_348fff62462d32a00f93a0ac5be86e99 } from '@/collections/Courses/components/CourseTypeField' import { DuplicatePageFor as DuplicatePageFor_8f1d8961a356bec6784e5c591c016925 } from '@/collections/Pages/components/DuplicatePageFor' @@ -23,6 +22,7 @@ import { AvyFxLogo as AvyFxLogo_f711e8d8656c7552b63fe9abc7b36dc4 } from '@/compo import { LogoutButton as LogoutButton_db9ac62598c46d0f1db201f6af05442e } from '@/components/LogoutButton' import { default as default_2aead22399b7847b21b134dc4a7931e0 } from '@/components/TenantSelector/TenantSelector' import { default as default_cb0ad5752e1389a2a940bb73c2c0e7d2 } from '@/components/ViewTypeAction' +import { QueriedEventsComponent as QueriedEventsComponent_134552ff11f6f98eabbe00772c3659e9 } from '@/fields/EventQuery/QueriedEventsComponent' import { LocationMap as LocationMap_4b1c9ff6af70dfec8b61ae82b54165d8 } from '@/fields/location/components/LocationMap' import { LinkLabelDescription as LinkLabelDescription_cc2cf53f1598892c0c926f3cb616a721 } from '@/fields/navLink/components/LinkLabelDescription' import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent' @@ -104,8 +104,8 @@ export const importMap = { '@/components/ColumnLayoutPicker#default': default_923dc5ccc0b72de4298251644cbfe39e, '@/blocks/Content/components/DefaultColumnAdder#DefaultColumnAdder': DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01, - '@/blocks/EventQuery/fields/QueriedEventsComponent#QueriedEventsComponent': - QueriedEventsComponent_21a7403c1e15ec396d69bd72be28641d, + '@/fields/EventQuery/QueriedEventsComponent#QueriedEventsComponent': + QueriedEventsComponent_134552ff11f6f98eabbe00772c3659e9, '@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, '@payloadcms/plugin-seo/client#MetaImageComponent': From 0a21fe8e1e0748e18c3aa9d3f58640cbede72ea1 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 13 Nov 2025 12:18:03 -0800 Subject: [PATCH 32/48] Remove sort field and add event tags and groups --- src/blocks/EventList/Component.tsx | 5 +- src/blocks/EventList/config.ts | 20 +--- src/endpoints/seed/blocks/event-list.ts | 1 - src/endpoints/seed/home-page.ts | 1 - src/endpoints/seed/pages/all-blocks-page.ts | 1 - src/fields/EventQuery/config.ts | 22 +++++ src/payload-types.ts | 101 ++++++++++---------- 7 files changed, 73 insertions(+), 78 deletions(-) diff --git a/src/blocks/EventList/Component.tsx b/src/blocks/EventList/Component.tsx index ff7679c8..16bd6392 100644 --- a/src/blocks/EventList/Component.tsx +++ b/src/blocks/EventList/Component.tsx @@ -13,7 +13,7 @@ type EventListComponentProps = EventListBlockProps & { export const EventListBlockComponent = async (args: EventListComponentProps) => { const { heading, belowHeadingContent, backgroundColor, className, wrapInContainer = true } = args - const { filterByEventTypes, sortBy, queriedEvents } = args.dynamicOptions || {} + const { filterByEventTypes, queriedEvents } = args.dynamicOptions || {} const { staticEvents } = args.staticOptions || {} let events = staticEvents?.filter( @@ -29,9 +29,6 @@ export const EventListBlockComponent = async (args: EventListComponentProps) => } const eventsLinkQueryParams = new URLSearchParams() - if (sortBy !== undefined) { - eventsLinkQueryParams.set('sort', sortBy) - } if (filterByEventTypes && filterByEventTypes.length > 0) { eventsLinkQueryParams.set('types', filterByEventTypes.join(',')) diff --git a/src/blocks/EventList/config.ts b/src/blocks/EventList/config.ts index 6036cf8a..ec2ed275 100644 --- a/src/blocks/EventList/config.ts +++ b/src/blocks/EventList/config.ts @@ -6,22 +6,6 @@ import { } from '@/fields/EventQuery/config' import type { Block, Field } from 'payload' -const sortByField = (): Field => ({ - name: 'sortBy', - type: 'select', - defaultValue: 'startDate', - options: [ - { label: 'Start Date (Earliest First)', value: 'startDate' }, - { label: 'Start Date (Latest First)', value: '-startDate' }, - { label: 'Registration Deadline (Earliest First)', value: 'registrationDeadline' }, - { label: 'Registration Deadline (Latest First)', value: '-registrationDeadline' }, - ], - required: true, - admin: { - description: 'Select how the list of events will be sorted.', - }, -}) - const eventListBlockWithFields = (fields: Field[]): Block => ({ slug: 'eventList', interfaceName: 'EventListBlock', @@ -31,7 +15,7 @@ const eventListBlockWithFields = (fields: Field[]): Block => ({ export const EventListBlock = eventListBlockWithFields([ ...defaultStylingFields([colorPickerField('Background color')]), - ...dynamicEventRelatedFields([sortByField()]), + ...dynamicEventRelatedFields(), ...staticEventRelatedFields, ]) @@ -46,6 +30,6 @@ export const EventListBlockLexical = eventListBlockWithFields([ type: 'checkbox', defaultValue: false, }, - ...dynamicEventRelatedFields([sortByField()]), + ...dynamicEventRelatedFields(), ...staticEventRelatedFields, ]) diff --git a/src/endpoints/seed/blocks/event-list.ts b/src/endpoints/seed/blocks/event-list.ts index ddfd6f25..19713d79 100644 --- a/src/endpoints/seed/blocks/event-list.ts +++ b/src/endpoints/seed/blocks/event-list.ts @@ -35,7 +35,6 @@ export const eventListBlock: EventListBlock = { }, eventOptions: 'dynamic', dynamicOptions: { - sortBy: 'startDate', showUpcomingOnly: true, maxEvents: 4, queriedEvents: [], // Will be populated during seeding diff --git a/src/endpoints/seed/home-page.ts b/src/endpoints/seed/home-page.ts index bf581c1c..adea636a 100644 --- a/src/endpoints/seed/home-page.ts +++ b/src/endpoints/seed/home-page.ts @@ -161,7 +161,6 @@ export const homePage: ( backgroundColor: 'transparent', eventOptions: 'dynamic', dynamicOptions: { - sortBy: 'startDate', showUpcomingOnly: true, maxEvents: 4, queriedEvents: events.slice(0, 4).map((event) => event.id), // Use first 4 events for preview diff --git a/src/endpoints/seed/pages/all-blocks-page.ts b/src/endpoints/seed/pages/all-blocks-page.ts index a32976f8..0b42d916 100644 --- a/src/endpoints/seed/pages/all-blocks-page.ts +++ b/src/endpoints/seed/pages/all-blocks-page.ts @@ -63,7 +63,6 @@ export const allBlocksPage: ( ...eventListBlock, eventOptions: 'dynamic', dynamicOptions: { - sortBy: 'startDate', showUpcomingOnly: true, maxEvents: 4, queriedEvents: events.slice(0, 4).map((event) => event.id), // Use first 4 events for preview diff --git a/src/fields/EventQuery/config.ts b/src/fields/EventQuery/config.ts index 62ce9d1e..699f2ea0 100644 --- a/src/fields/EventQuery/config.ts +++ b/src/fields/EventQuery/config.ts @@ -78,6 +78,28 @@ export const dynamicEventRelatedFields = (additionalFilters?: Field[]): Field[] description: 'Optionally select event types to filter events.', }, }, + { + name: 'filterByEventGroups', + type: 'relationship', + relationTo: 'eventGroups', + hasMany: true, + admin: { + position: 'sidebar', + description: 'Optionally select event group to filter events.', + }, + filterOptions: getTenantFilter, + }, + { + name: 'filterByEventTags', + type: 'relationship', + relationTo: 'eventTags', + hasMany: true, + admin: { + position: 'sidebar', + description: 'Optionally select event tags to filter events.', + }, + filterOptions: getTenantFilter, + }, { name: 'showUpcomingOnly', type: 'checkbox', diff --git a/src/payload-types.ts b/src/payload-types.ts index 4e127a59..ce53ef28 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -699,16 +699,20 @@ export interface EventListBlock { */ wrapInContainer?: boolean | null; dynamicOptions?: { - /** - * Select how the list of events will be sorted. - */ - sortBy: 'startDate' | '-startDate' | 'registrationDeadline' | '-registrationDeadline'; /** * Optionally select event types to filter events. */ filterByEventTypes?: | ('events-by-ac' | 'awareness' | 'workshop' | 'field-class-by-ac' | 'volunteer' | 'events-by-others')[] | null; + /** + * Optionally select event group to filter events. + */ + filterByEventGroups?: (number | EventGroup)[] | null; + /** + * Optionally select event tags to filter events. + */ + filterByEventTags?: (number | EventTag)[] | null; /** * Only display events that have not yet occurred. */ @@ -729,6 +733,34 @@ export interface EventListBlock { blockName?: string | null; blockType: 'eventList'; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "eventGroups". + */ +export interface EventGroup { + id: number; + tenant: number | Tenant; + title: string; + description?: string | null; + slug: string; + contentHash?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "eventTags". + */ +export interface EventTag { + id: number; + tenant: number | Tenant; + title: string; + description?: string | null; + slug: string; + contentHash?: string | null; + updatedAt: string; + createdAt: string; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "events". @@ -904,51 +936,6 @@ export interface Event { createdAt: string; _status?: ('draft' | 'published') | null; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "eventGroups". - */ -export interface EventGroup { - id: number; - tenant: number | Tenant; - title: string; - description?: string | null; - content: { - root: { - type: string; - children: { - type: any; - version: number; - [k: string]: unknown; - }[]; - direction: ('ltr' | 'rtl') | null; - format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; - indent: number; - version: number; - }; - [k: string]: unknown; - }; - slug: string; - featuredImage?: (number | null) | Media; - thumbnailImage?: (number | null) | Media; - contentHash?: string | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "eventTags". - */ -export interface EventTag { - id: number; - tenant: number | Tenant; - title: string; - description?: string | null; - slug: string; - contentHash?: string | null; - updatedAt: string; - createdAt: string; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "EventTableBlock". @@ -981,6 +968,14 @@ export interface EventTableBlock { filterByEventTypes?: | ('events-by-ac' | 'awareness' | 'workshop' | 'field-class-by-ac' | 'volunteer' | 'events-by-others')[] | null; + /** + * Optionally select event group to filter events. + */ + filterByEventGroups?: (number | EventGroup)[] | null; + /** + * Optionally select event tags to filter events. + */ + filterByEventTags?: (number | EventTag)[] | null; /** * Only display events that have not yet occurred. */ @@ -2902,8 +2897,9 @@ export interface EventListBlockSelect { dynamicOptions?: | T | { - sortBy?: T; filterByEventTypes?: T; + filterByEventGroups?: T; + filterByEventTags?: T; showUpcomingOnly?: T; maxEvents?: T; queriedEvents?: T; @@ -2938,6 +2934,8 @@ export interface EventTableBlockSelect { | T | { filterByEventTypes?: T; + filterByEventGroups?: T; + filterByEventTags?: T; showUpcomingOnly?: T; maxEvents?: T; queriedEvents?: T; @@ -3405,10 +3403,7 @@ export interface EventGroupsSelect { tenant?: T; title?: T; description?: T; - content?: T; slug?: T; - featuredImage?: T; - thumbnailImage?: T; contentHash?: T; updatedAt?: T; createdAt?: T; From d5cf47e1e1e44bb47f2e037a63063ceff88c2924 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 13 Nov 2025 12:24:06 -0800 Subject: [PATCH 33/48] Fix seed script from page layout removal --- src/endpoints/seed/index.ts | 51 ------------------------------------- 1 file changed, 51 deletions(-) diff --git a/src/endpoints/seed/index.ts b/src/endpoints/seed/index.ts index 67b5bd6a..07a5d11f 100644 --- a/src/endpoints/seed/index.ts +++ b/src/endpoints/seed/index.ts @@ -905,57 +905,6 @@ export const seed = async ({ description: 'Meet your local avalanche forecasters & learn more about your avy center', slug: 'meet-your-forecaster', tenant: tenant.id, - content: { - root: { - type: 'root', - format: '', - indent: 0, - version: 1, - children: [ - { - type: 'paragraph', - format: '', - indent: 0, - version: 1, - children: [ - { - mode: 'normal', - text: 'Meet the expert forecasters behind avalanche predictions. Learn about the people and expertise that go into creating daily forecasts to keep backcountry users safe. Discover how our team analyzes snow and weather data to provide you with the most accurate and timely avalanche information.', - type: 'text', - style: '', - detail: 0, - format: 0, - version: 1, - }, - ], - direction: 'ltr', - textStyle: '', - textFormat: 0, - }, - { - type: 'block', - version: 0, - format: '', - fields: { - blockName: '', - eventOptions: 'dynamic', - - dynamicOptions: { - maxEvents: 12, - queriedEvents: [], // Will be populated during seeding - }, - staticOptions: { - staticEvents: [], - }, - blockType: 'eventTable', - }, - }, - ], - direction: 'ltr', - }, - }, - thumbnailImage: images[tenant.slug]['imageMountain'], - featuredImage: images[tenant.slug]['image1'], }, ]) .flat(), From d7dd2383d96f1c2bbd62dd8976d1371c3c425d49 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Sun, 16 Nov 2025 13:21:45 -0800 Subject: [PATCH 34/48] Update permissions and move events query to client --- src/blocks/EventTable/Component.tsx | 129 ++++++++++++++++-- src/collections/Events/index.ts | 4 +- .../EventQuery/QueriedEventsComponent.tsx | 34 ++++- 3 files changed, 147 insertions(+), 20 deletions(-) diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index c232933e..42dafb48 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -1,27 +1,31 @@ +'use client' + import { EventTable } from '@/components/EventsTable' import RichText from '@/components/RichText' import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' +import { useTenant } from '@/providers/TenantProvider' import { cn } from '@/utilities/ui' +import { useEffect, useState } from 'react' type EventTableComponentProps = EventTableBlockProps & { className?: string } -export const EventTableBlockComponent = async (args: EventTableComponentProps) => { +export const EventTableBlockComponent = (args: EventTableComponentProps) => { const { heading, belowHeadingContent, className } = args - - const { filterByEventTypes, queriedEvents } = args.dynamicOptions || {} + const { + filterByEventTypes, + filterByEventGroups, + filterByEventTags, + showUpcomingOnly, + maxEvents, + } = args.dynamicOptions || {} const { staticEvents } = args.staticOptions || {} - let events = staticEvents?.filter( - (event): event is Event => typeof event === 'object' && event !== null, - ) - - if (!staticEvents || (staticEvents.length === 0 && queriedEvents && queriedEvents.length > 0)) { - events = queriedEvents?.filter( - (event): event is Event => typeof event === 'object' && event !== null, - ) - } + const [events, setEvents] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const { tenant } = useTenant() const eventsLinkQueryParams = new URLSearchParams() @@ -29,7 +33,96 @@ export const EventTableBlockComponent = async (args: EventTableComponentProps) = eventsLinkQueryParams.set('types', filterByEventTypes.join(',')) } - if (!events) { + // Fetch dynamic events + useEffect(() => { + const shouldFetchDynamic = + !staticEvents || + (staticEvents.length === 0 && + (filterByEventTypes?.length || filterByEventGroups?.length || filterByEventTags?.length)) + + if (!shouldFetchDynamic) { + setLoading(false) + return + } + + const fetchEvents = async () => { + try { + setLoading(true) + setError(null) + + const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id + if (!tenantId) return + + const params = new URLSearchParams({ + limit: String(maxEvents || 4), + depth: '1', + 'where[tenant][equals]': String(tenantId), + }) + + if (filterByEventTypes && filterByEventTypes.length > 0) { + params.append('where[type][in]', filterByEventTypes.join(',')) + } + + if (filterByEventGroups && filterByEventGroups.length > 0) { + const groupIds = filterByEventGroups + .map((g) => (typeof g === 'object' ? g.id : g)) + .filter(Boolean) + if (groupIds.length > 0) { + params.append('where[groups][in]', groupIds.join(',')) + } + } + + if (filterByEventTags && filterByEventTags.length > 0) { + const tagIds = filterByEventTags + .map((t) => (typeof t === 'object' ? t.id : t)) + .filter(Boolean) + if (tagIds.length > 0) { + params.append('where[tags][in]', tagIds.join(',')) + } + } + + if (showUpcomingOnly) { + params.append('where[startDate][greater_than]', new Date().toISOString()) + } + + const response = await fetch(`/api/events?${params.toString()}`, { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + }, + cache: 'no-store', // Add this + }) + if (!response.ok) { + throw new Error('Failed to fetch events') + } + + const data = await response.json() + setEvents(data.docs) + } catch (err) { + console.error('[EventTable Error]:', err) + setError(err instanceof Error ? err.message : 'An error occurred while fetching events') + } finally { + setLoading(false) + } + } + + fetchEvents() + }, [ + filterByEventTypes, + filterByEventGroups, + filterByEventTags, + showUpcomingOnly, + maxEvents, + staticEvents, + tenant, + ]) + + // Use static events if available, otherwise use fetched events + const displayEvents = + staticEvents?.filter((event): event is Event => typeof event === 'object' && event !== null) ?? + events + + if (!displayEvents) { return null } @@ -48,8 +141,14 @@ export const EventTableBlockComponent = async (args: EventTableComponentProps) = )}
- {events && events?.length > 0 ? ( - + {(loading || error) && ( +
+ {loading &&

Loading events...

} + {error &&

Error loading events: {error}

} +
+ )} + {displayEvents && displayEvents.length > 0 ? ( + ) : (

There are no events matching these results.

)} diff --git a/src/collections/Events/index.ts b/src/collections/Events/index.ts index 77562e15..368e5714 100644 --- a/src/collections/Events/index.ts +++ b/src/collections/Events/index.ts @@ -1,4 +1,4 @@ -import { accessByTenantRole } from '@/access/byTenantRole' +import { accessByTenantRoleWithPermissiveRead } from '@/access/byTenantRole' import { filterByTenant } from '@/access/filterByTenant' import { Banner } from '@/blocks/Banner/config' import { BlogListBlockLexical } from '@/blocks/BlogList/config' @@ -33,7 +33,7 @@ import { eventTypesData } from './constants' export const Events: CollectionConfig = { slug: 'events', - access: accessByTenantRole('events'), + access: accessByTenantRoleWithPermissiveRead('events'), admin: { baseListFilter: filterByTenant, group: 'Events', diff --git a/src/fields/EventQuery/QueriedEventsComponent.tsx b/src/fields/EventQuery/QueriedEventsComponent.tsx index 8478b0d9..0162c9c6 100644 --- a/src/fields/EventQuery/QueriedEventsComponent.tsx +++ b/src/fields/EventQuery/QueriedEventsComponent.tsx @@ -23,9 +23,15 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr const [selectOptions, setSelectOptions] = useState([]) const { setDisabled } = useForm() + const filterByEventGroups = useFormFields( + ([fields]) => fields[parentPathParts.concat(['filterByEventGroups']).join('.')]?.value, + ) const filterByEventTypes = useFormFields( ([fields]) => fields[parentPathParts.concat(['filterByEventTypes']).join('.')]?.value, ) + const filterByEventTags = useFormFields( + ([fields]) => fields[parentPathParts.concat(['filterByEventTags']).join('.')]?.value, + ) const sortBy = useFormFields( ([fields]) => fields[parentPathParts.concat(['sortBy']).join('.')]?.value, ) @@ -64,6 +70,18 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr params.append('where[startDate][greater_than]', new Date().toISOString()) } + if ( + filterByEventGroups && + Array.isArray(filterByEventGroups) && + filterByEventGroups.length > 0 + ) { + const groupIds = filterByEventGroups.filter(Boolean) + + if (groupIds.length > 0) { + params.append('where[type][in]', groupIds.join(',')) + } + } + if ( filterByEventTypes && Array.isArray(filterByEventTypes) && @@ -76,6 +94,14 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr } } + if (filterByEventTags && Array.isArray(filterByEventTags) && filterByEventTags.length > 0) { + const tagIds = filterByEventTags.filter(Boolean) + + if (tagIds.length > 0) { + params.append('where[tags][in]', tagIds.join(',')) + } + } + const response = await fetch(`/api/events?${params.toString()}`) if (!response.ok) { throw new Error('Failed to fetch events') @@ -112,14 +138,16 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr fetchEvents() }, [ + filterByEventGroups, + filterByEventTags, filterByEventTypes, - sortBy, maxEvents, + setDisabled, + setValue, showUpcomingOnly, + sortBy, tenant, - setValue, value, - setDisabled, ]) const currentValue = fetchedEvents.map((event) => String(event.id)) From ac80dd1b3ae0cbec78ed3517577fd989a20bd039 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Sun, 16 Nov 2025 14:10:57 -0800 Subject: [PATCH 35/48] Update component to use a hook to fetch events --- src/blocks/EventTable/Component.tsx | 106 +++------------------ src/blocks/EventTable/useDynamicEvents.tsx | 92 ++++++++++++++++++ 2 files changed, 104 insertions(+), 94 deletions(-) create mode 100644 src/blocks/EventTable/useDynamicEvents.tsx diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 42dafb48..6e2dd757 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -5,14 +5,14 @@ import RichText from '@/components/RichText' import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' import { useTenant } from '@/providers/TenantProvider' import { cn } from '@/utilities/ui' -import { useEffect, useState } from 'react' +import { useDynamicEvents } from './useDynamicEvents' type EventTableComponentProps = EventTableBlockProps & { className?: string } export const EventTableBlockComponent = (args: EventTableComponentProps) => { - const { heading, belowHeadingContent, className } = args + const { heading, belowHeadingContent, className, eventOptions } = args const { filterByEventTypes, filterByEventGroups, @@ -22,105 +22,23 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { } = args.dynamicOptions || {} const { staticEvents } = args.staticOptions || {} - const [events, setEvents] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) const { tenant } = useTenant() - - const eventsLinkQueryParams = new URLSearchParams() - - if (filterByEventTypes && filterByEventTypes.length > 0) { - eventsLinkQueryParams.set('types', filterByEventTypes.join(',')) - } - - // Fetch dynamic events - useEffect(() => { - const shouldFetchDynamic = - !staticEvents || - (staticEvents.length === 0 && - (filterByEventTypes?.length || filterByEventGroups?.length || filterByEventTags?.length)) - - if (!shouldFetchDynamic) { - setLoading(false) - return - } - - const fetchEvents = async () => { - try { - setLoading(true) - setError(null) - - const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id - if (!tenantId) return - - const params = new URLSearchParams({ - limit: String(maxEvents || 4), - depth: '1', - 'where[tenant][equals]': String(tenantId), - }) - - if (filterByEventTypes && filterByEventTypes.length > 0) { - params.append('where[type][in]', filterByEventTypes.join(',')) - } - - if (filterByEventGroups && filterByEventGroups.length > 0) { - const groupIds = filterByEventGroups - .map((g) => (typeof g === 'object' ? g.id : g)) - .filter(Boolean) - if (groupIds.length > 0) { - params.append('where[groups][in]', groupIds.join(',')) - } - } - - if (filterByEventTags && filterByEventTags.length > 0) { - const tagIds = filterByEventTags - .map((t) => (typeof t === 'object' ? t.id : t)) - .filter(Boolean) - if (tagIds.length > 0) { - params.append('where[tags][in]', tagIds.join(',')) - } - } - - if (showUpcomingOnly) { - params.append('where[startDate][greater_than]', new Date().toISOString()) - } - - const response = await fetch(`/api/events?${params.toString()}`, { - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'no-cache, no-store, must-revalidate', - }, - cache: 'no-store', // Add this - }) - if (!response.ok) { - throw new Error('Failed to fetch events') - } - - const data = await response.json() - setEvents(data.docs) - } catch (err) { - console.error('[EventTable Error]:', err) - setError(err instanceof Error ? err.message : 'An error occurred while fetching events') - } finally { - setLoading(false) - } - } - - fetchEvents() - }, [ + const { + events: fetchedEvents, + loading, + error, + } = useDynamicEvents({ + tenant, filterByEventTypes, filterByEventGroups, filterByEventTags, showUpcomingOnly, maxEvents, - staticEvents, - tenant, - ]) + }) - // Use static events if available, otherwise use fetched events - const displayEvents = - staticEvents?.filter((event): event is Event => typeof event === 'object' && event !== null) ?? - events + let displayEvents + if (eventOptions === 'static') displayEvents = staticEvents as Event[] + if (eventOptions === 'dynamic') displayEvents = fetchedEvents if (!displayEvents) { return null diff --git a/src/blocks/EventTable/useDynamicEvents.tsx b/src/blocks/EventTable/useDynamicEvents.tsx new file mode 100644 index 00000000..7226485c --- /dev/null +++ b/src/blocks/EventTable/useDynamicEvents.tsx @@ -0,0 +1,92 @@ +'use client' + +import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' +import { useEffect, useState } from 'react' + +type UseEventsProps = EventTableBlockProps['dynamicOptions'] & { + tenant?: number | { id?: number } | null +} +export const useDynamicEvents = ({ + tenant, + filterByEventTypes, + filterByEventGroups, + filterByEventTags, + showUpcomingOnly, + maxEvents, +}: UseEventsProps) => { + // ... rest of code + const [events, setEvents] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + const fetchEvents = async () => { + try { + setLoading(true) + setError(null) + + const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id + if (!tenantId) return + + const params = new URLSearchParams({ + tenantId: String(tenantId), + limit: String(maxEvents || 4), + depth: '1', + }) + + if (filterByEventTypes?.length) { + params.append('types', filterByEventTypes.join(',')) + } + + if (filterByEventGroups?.length) { + const groupIds = filterByEventGroups + .map((g) => (typeof g === 'object' ? g.id : g)) + .filter(Boolean) + if (groupIds.length) { + params.append('groups', groupIds.join(',')) + } + } + + if (filterByEventTags?.length) { + const tagIds = filterByEventTags + .map((t) => (typeof t === 'object' ? t.id : t)) + .filter(Boolean) + if (tagIds.length) { + params.append('tags', tagIds.join(',')) + } + } + + if (showUpcomingOnly) { + params.append('upcomingOnly', 'true') + } + + const response = await fetch(`/api/events?${params.toString()}`, { + cache: 'no-store', + }) + + if (!response.ok) { + throw new Error('Failed to fetch events') + } + + const data = await response.json() + setEvents(data.docs || []) + } catch (err) { + console.error('[useEvents Error]:', err) + setError(err instanceof Error ? err.message : 'An error occurred while fetching events') + } finally { + setLoading(false) + } + } + + fetchEvents() + }, [ + filterByEventTypes, + filterByEventGroups, + filterByEventTags, + showUpcomingOnly, + maxEvents, + tenant, + ]) + + return { events, loading, error } +} From 417d26e3bbb0f7e486152399e3fdf226349ebdad Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 12:44:22 -0800 Subject: [PATCH 36/48] Add events table to blocks seed page --- src/endpoints/seed/pages/all-blocks-page.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/endpoints/seed/pages/all-blocks-page.ts b/src/endpoints/seed/pages/all-blocks-page.ts index 0b42d916..56a75fef 100644 --- a/src/endpoints/seed/pages/all-blocks-page.ts +++ b/src/endpoints/seed/pages/all-blocks-page.ts @@ -84,6 +84,19 @@ export const allBlocksPage: ( event: events[1]?.id || 0, // Use second event backgroundColor: 'gray', }, + { + blockName: '', + eventOptions: 'dynamic', + + dynamicOptions: { + maxEvents: 6, + queriedEvents: [], // Will be populated during seeding + }, + staticOptions: { + staticEvents: [], + }, + blockType: 'eventTable', + }, ], meta: { image: null, From 6b21c64901d7035e00db53e67ef7a8531829fe8f Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 13:13:17 -0800 Subject: [PATCH 37/48] Remove showUpcomingOnly --- src/blocks/EventTable/Component.tsx | 10 ++-------- src/blocks/EventTable/useDynamicEvents.tsx | 15 ++------------- src/endpoints/seed/blocks/event-list.ts | 1 - src/endpoints/seed/home-page.ts | 1 - src/endpoints/seed/pages/all-blocks-page.ts | 1 - src/fields/EventQuery/QueriedEventsComponent.tsx | 9 +-------- src/fields/EventQuery/config.ts | 9 --------- src/payload-types.ts | 10 ---------- 8 files changed, 5 insertions(+), 51 deletions(-) diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 6e2dd757..316063b6 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -13,13 +13,8 @@ type EventTableComponentProps = EventTableBlockProps & { export const EventTableBlockComponent = (args: EventTableComponentProps) => { const { heading, belowHeadingContent, className, eventOptions } = args - const { - filterByEventTypes, - filterByEventGroups, - filterByEventTags, - showUpcomingOnly, - maxEvents, - } = args.dynamicOptions || {} + const { filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents } = + args.dynamicOptions || {} const { staticEvents } = args.staticOptions || {} const { tenant } = useTenant() @@ -32,7 +27,6 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { filterByEventTypes, filterByEventGroups, filterByEventTags, - showUpcomingOnly, maxEvents, }) diff --git a/src/blocks/EventTable/useDynamicEvents.tsx b/src/blocks/EventTable/useDynamicEvents.tsx index 7226485c..9f71ea8d 100644 --- a/src/blocks/EventTable/useDynamicEvents.tsx +++ b/src/blocks/EventTable/useDynamicEvents.tsx @@ -11,10 +11,8 @@ export const useDynamicEvents = ({ filterByEventTypes, filterByEventGroups, filterByEventTags, - showUpcomingOnly, maxEvents, }: UseEventsProps) => { - // ... rest of code const [events, setEvents] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -56,9 +54,7 @@ export const useDynamicEvents = ({ } } - if (showUpcomingOnly) { - params.append('upcomingOnly', 'true') - } + params.append('where[startDate][greater_than]', new Date().toISOString()) const response = await fetch(`/api/events?${params.toString()}`, { cache: 'no-store', @@ -79,14 +75,7 @@ export const useDynamicEvents = ({ } fetchEvents() - }, [ - filterByEventTypes, - filterByEventGroups, - filterByEventTags, - showUpcomingOnly, - maxEvents, - tenant, - ]) + }, [filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents, tenant]) return { events, loading, error } } diff --git a/src/endpoints/seed/blocks/event-list.ts b/src/endpoints/seed/blocks/event-list.ts index 19713d79..a777c1cf 100644 --- a/src/endpoints/seed/blocks/event-list.ts +++ b/src/endpoints/seed/blocks/event-list.ts @@ -35,7 +35,6 @@ export const eventListBlock: EventListBlock = { }, eventOptions: 'dynamic', dynamicOptions: { - showUpcomingOnly: true, maxEvents: 4, queriedEvents: [], // Will be populated during seeding }, diff --git a/src/endpoints/seed/home-page.ts b/src/endpoints/seed/home-page.ts index adea636a..82421639 100644 --- a/src/endpoints/seed/home-page.ts +++ b/src/endpoints/seed/home-page.ts @@ -161,7 +161,6 @@ export const homePage: ( backgroundColor: 'transparent', eventOptions: 'dynamic', dynamicOptions: { - showUpcomingOnly: true, maxEvents: 4, queriedEvents: events.slice(0, 4).map((event) => event.id), // Use first 4 events for preview }, diff --git a/src/endpoints/seed/pages/all-blocks-page.ts b/src/endpoints/seed/pages/all-blocks-page.ts index 56a75fef..306cedab 100644 --- a/src/endpoints/seed/pages/all-blocks-page.ts +++ b/src/endpoints/seed/pages/all-blocks-page.ts @@ -63,7 +63,6 @@ export const allBlocksPage: ( ...eventListBlock, eventOptions: 'dynamic', dynamicOptions: { - showUpcomingOnly: true, maxEvents: 4, queriedEvents: events.slice(0, 4).map((event) => event.id), // Use first 4 events for preview }, diff --git a/src/fields/EventQuery/QueriedEventsComponent.tsx b/src/fields/EventQuery/QueriedEventsComponent.tsx index 0162c9c6..d1e6d483 100644 --- a/src/fields/EventQuery/QueriedEventsComponent.tsx +++ b/src/fields/EventQuery/QueriedEventsComponent.tsx @@ -38,9 +38,6 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr const maxEvents = useFormFields( ([fields]) => fields[parentPathParts.concat(['maxEvents']).join('.')]?.value, ) - const showUpcomingOnly = useFormFields( - ([fields]) => fields[parentPathParts.concat(['showUpcomingOnly']).join('.')]?.value, - ) const { selectedTenantID: tenant } = useTenantSelection() useEffect(() => { @@ -66,10 +63,6 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr params.append('sort', String(sortBy)) } - if (showUpcomingOnly) { - params.append('where[startDate][greater_than]', new Date().toISOString()) - } - if ( filterByEventGroups && Array.isArray(filterByEventGroups) && @@ -101,6 +94,7 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr params.append('where[tags][in]', tagIds.join(',')) } } + params.append('where[startDate][greater_than]', new Date().toISOString()) const response = await fetch(`/api/events?${params.toString()}`) if (!response.ok) { @@ -144,7 +138,6 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr maxEvents, setDisabled, setValue, - showUpcomingOnly, sortBy, tenant, value, diff --git a/src/fields/EventQuery/config.ts b/src/fields/EventQuery/config.ts index 699f2ea0..54585d61 100644 --- a/src/fields/EventQuery/config.ts +++ b/src/fields/EventQuery/config.ts @@ -100,15 +100,6 @@ export const dynamicEventRelatedFields = (additionalFilters?: Field[]): Field[] }, filterOptions: getTenantFilter, }, - { - name: 'showUpcomingOnly', - type: 'checkbox', - defaultValue: true, - label: 'Show Upcoming Events Only', - admin: { - description: 'Only display events that have not yet occurred.', - }, - }, { name: 'maxEvents', type: 'number', diff --git a/src/payload-types.ts b/src/payload-types.ts index ce53ef28..6f33092e 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -713,10 +713,6 @@ export interface EventListBlock { * Optionally select event tags to filter events. */ filterByEventTags?: (number | EventTag)[] | null; - /** - * Only display events that have not yet occurred. - */ - showUpcomingOnly?: boolean | null; /** * Maximum number of events that will be displayed. Must be an integer. */ @@ -976,10 +972,6 @@ export interface EventTableBlock { * Optionally select event tags to filter events. */ filterByEventTags?: (number | EventTag)[] | null; - /** - * Only display events that have not yet occurred. - */ - showUpcomingOnly?: boolean | null; /** * Maximum number of events that will be displayed. Must be an integer. */ @@ -2900,7 +2892,6 @@ export interface EventListBlockSelect { filterByEventTypes?: T; filterByEventGroups?: T; filterByEventTags?: T; - showUpcomingOnly?: T; maxEvents?: T; queriedEvents?: T; }; @@ -2936,7 +2927,6 @@ export interface EventTableBlockSelect { filterByEventTypes?: T; filterByEventGroups?: T; filterByEventTags?: T; - showUpcomingOnly?: T; maxEvents?: T; queriedEvents?: T; }; From 50641a3ba60ca59415b3bbedba46c820ad79d5a1 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 13:17:48 -0800 Subject: [PATCH 38/48] Revert group page removal changes --- src/collections/EventGroups/index.ts | 19 +++++++------------ .../Breadcrumbs/Breadcrumbs.client.tsx | 6 +----- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/collections/EventGroups/index.ts b/src/collections/EventGroups/index.ts index c8be35df..382e6afc 100644 --- a/src/collections/EventGroups/index.ts +++ b/src/collections/EventGroups/index.ts @@ -16,18 +16,13 @@ export const EventGroups: CollectionConfig = { fields: [ tenantField(), { - type: 'group', - fields: [ - { - name: 'title', - type: 'text', - required: true, - }, - { - name: 'description', - type: 'textarea', - }, - ], + name: 'title', + type: 'text', + required: true, + }, + { + name: 'description', + type: 'textarea', }, slugField(), contentHashField(), diff --git a/src/components/Breadcrumbs/Breadcrumbs.client.tsx b/src/components/Breadcrumbs/Breadcrumbs.client.tsx index 15d09862..8b3263d0 100644 --- a/src/components/Breadcrumbs/Breadcrumbs.client.tsx +++ b/src/components/Breadcrumbs/Breadcrumbs.client.tsx @@ -53,11 +53,7 @@ const processNestedSegments = ( export function Breadcrumbs() { const segments = useSelectedLayoutSegments() - // Exclude the 'g' segment from breadcrumbs, but only when 'g' immediately follows 'events' in the URL path - // (e.g., for /events/g/[slug] routes) - const decodedSegments = segments - .map(decodeURIComponent) - .filter((segment, index, arr) => !(segment === 'g' && arr[index - 1] === 'events')) + const decodedSegments = segments.map(decodeURIComponent) const { isNotFound } = useNotFound() const { captureWithTenant } = useAnalytics() From f20d8b8190051dff5be686b500c8ebd8acb62a03 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 13:48:11 -0800 Subject: [PATCH 39/48] Clean up event table - remove show more, open event registration in new tab, remove unused file --- .../EventsTable/EventsTableWrapper.tsx | 15 ------ src/components/EventsTable/index.tsx | 46 +++++-------------- 2 files changed, 11 insertions(+), 50 deletions(-) delete mode 100644 src/components/EventsTable/EventsTableWrapper.tsx diff --git a/src/components/EventsTable/EventsTableWrapper.tsx b/src/components/EventsTable/EventsTableWrapper.tsx deleted file mode 100644 index 1e77fda0..00000000 --- a/src/components/EventsTable/EventsTableWrapper.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import config from '@payload-config' -import { getPayload } from 'payload' -import { EventTable } from './index' - -export default async function EventTableWrapper() { - const payload = await getPayload({ config }) - - const { docs: events } = await payload.find({ - collection: 'events', - limit: 1000, - sort: '-startDate', - }) - - return -} diff --git a/src/components/EventsTable/index.tsx b/src/components/EventsTable/index.tsx index eb546c0a..00912d0f 100644 --- a/src/components/EventsTable/index.tsx +++ b/src/components/EventsTable/index.tsx @@ -24,11 +24,8 @@ import { CMSLink } from '../Link' export function EventTable({ events = [] }: { events: Event[] }) { const [sortConfig, setSortConfig] = useState({ key: 'startDate', direction: 'asc' }) - const [displayCount, setDisplayCount] = useState(10) const [expandedRows, setExpandedRows] = useState>(new Set()) - const ITEMS_PER_LOAD = 10 - // Determine status based on event data const getStatus = (event: Event) => { const now = new Date() @@ -125,9 +122,6 @@ export function EventTable({ events = [] }: { events: Event[] }) { return sorted }, [events, sortConfig]) - const displayedEvents = sortedEvents.slice(0, displayCount) - const hasMore = displayCount < sortedEvents.length - const handleSort = (key: string) => { if (sortConfig.key === key) { setSortConfig({ @@ -191,7 +185,7 @@ export function EventTable({ events = [] }: { events: Event[] }) { - {displayedEvents.map((event) => { + {sortedEvents.map((event) => { const { date, time } = formatDateTime(event.startDate) const status = getStatus(event) const { isPast, isRegistrationClosed } = status @@ -285,17 +279,16 @@ export function EventTable({ events = [] }: { events: Event[] }) {
{event.registrationUrl && !isPast && !isRegistrationClosed ? ( - <> - - Register - - - + + Register + + ) : isPast || isRegistrationClosed ? (
- - {/* Load More Button */} - {hasMore && ( -
- -
- )} - - {/* Results info */} -
- Showing {displayedEvents.length} of {sortedEvents.length} events -
) } From 0108f98863e8c9d94bcd719bd1c145010a8c013e Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 14:43:33 -0800 Subject: [PATCH 40/48] Create custom endpoint for event query --- src/app/api/eventsQuery/route.ts | 54 +++++++++++++++ src/blocks/EventTable/Component.tsx | 76 ++++++++++++++++---- src/blocks/EventTable/useDynamicEvents.tsx | 81 ---------------------- src/collections/Events/index.ts | 4 +- 4 files changed, 120 insertions(+), 95 deletions(-) create mode 100644 src/app/api/eventsQuery/route.ts delete mode 100644 src/blocks/EventTable/useDynamicEvents.tsx diff --git a/src/app/api/eventsQuery/route.ts b/src/app/api/eventsQuery/route.ts new file mode 100644 index 00000000..93127c7c --- /dev/null +++ b/src/app/api/eventsQuery/route.ts @@ -0,0 +1,54 @@ +import config from '@/payload.config' +import { getPayload, Where } from 'payload' + +export async function GET(req: Request) { + try { + const url = new URL(req.url) + const searchParams = url.searchParams + + const tenantId = searchParams.get('tenantId') + const maxEvents = parseInt(searchParams.get('limit') || '4') + const depth = parseInt(searchParams.get('depth') || '1') + const types = searchParams.get('types')?.split(',').filter(Boolean) + const groups = searchParams.get('groups')?.split(',').filter(Boolean) + const tags = searchParams.get('tags')?.split(',').filter(Boolean) + + if (!tenantId) { + return Response.json({ error: 'tenantId is required' }, { status: 400 }) + } + + const payload = await getPayload({ config }) + + const where: Where = { + tenant: { equals: tenantId }, + startDate: { greater_than: new Date().toISOString() }, + } + + if (types?.length) { + where.eventType = { in: types } + } + + if (groups?.length) { + where.eventGroup = { in: groups } + } + + if (tags?.length) { + where.eventTags = { in: tags } + } + + const data = await payload.find({ + collection: 'events', + where, + limit: maxEvents, + depth, + }) + + return Response.json({ docs: data.docs || [] }) + } catch (error) { + console.error('[Dynamic Events Endpoint Error]:', error) + return Response.json( + { error: error instanceof Error ? error.message : 'An error occurred' }, + { status: 500 }, + ) + } +} diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 316063b6..1cf3767c 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -5,7 +5,7 @@ import RichText from '@/components/RichText' import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' import { useTenant } from '@/providers/TenantProvider' import { cn } from '@/utilities/ui' -import { useDynamicEvents } from './useDynamicEvents' +import { useEffect, useState } from 'react' type EventTableComponentProps = EventTableBlockProps & { className?: string @@ -18,17 +18,69 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { const { staticEvents } = args.staticOptions || {} const { tenant } = useTenant() - const { - events: fetchedEvents, - loading, - error, - } = useDynamicEvents({ - tenant, - filterByEventTypes, - filterByEventGroups, - filterByEventTags, - maxEvents, - }) + const [fetchedEvents, setFetchedEvents] = useState([]) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + + useEffect(() => { + if (eventOptions !== 'dynamic') return + + const fetchEvents = async () => { + try { + setLoading(true) + setError(null) + + const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id + if (!tenantId) return + + const params = new URLSearchParams({ + tenantId: String(tenantId), + limit: String(maxEvents || 4), + depth: '1', + }) + + if (filterByEventTypes?.length) { + params.append('types', filterByEventTypes.join(',')) + } + + if (filterByEventGroups?.length) { + const groupIds = filterByEventGroups + .map((g) => (typeof g === 'object' ? g.id : g)) + .filter(Boolean) + if (groupIds.length) { + params.append('groups', groupIds.join(',')) + } + } + + if (filterByEventTags?.length) { + const tagIds = filterByEventTags + .map((t) => (typeof t === 'object' ? t.id : t)) + .filter(Boolean) + if (tagIds.length) { + params.append('tags', tagIds.join(',')) + } + } + + const response = await fetch(`/api/eventsQuery?${params.toString()}`, { + cache: 'no-store', + }) + + if (!response.ok) { + throw new Error('Failed to fetch events') + } + + const data = await response.json() + setFetchedEvents(data.docs || []) + } catch (err) { + console.error('[EventTable Error]:', err) + setError(err instanceof Error ? err.message : 'An error occurred while fetching events') + } finally { + setLoading(false) + } + } + + fetchEvents() + }, [eventOptions, filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents, tenant]) let displayEvents if (eventOptions === 'static') displayEvents = staticEvents as Event[] diff --git a/src/blocks/EventTable/useDynamicEvents.tsx b/src/blocks/EventTable/useDynamicEvents.tsx deleted file mode 100644 index 9f71ea8d..00000000 --- a/src/blocks/EventTable/useDynamicEvents.tsx +++ /dev/null @@ -1,81 +0,0 @@ -'use client' - -import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' -import { useEffect, useState } from 'react' - -type UseEventsProps = EventTableBlockProps['dynamicOptions'] & { - tenant?: number | { id?: number } | null -} -export const useDynamicEvents = ({ - tenant, - filterByEventTypes, - filterByEventGroups, - filterByEventTags, - maxEvents, -}: UseEventsProps) => { - const [events, setEvents] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - const fetchEvents = async () => { - try { - setLoading(true) - setError(null) - - const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id - if (!tenantId) return - - const params = new URLSearchParams({ - tenantId: String(tenantId), - limit: String(maxEvents || 4), - depth: '1', - }) - - if (filterByEventTypes?.length) { - params.append('types', filterByEventTypes.join(',')) - } - - if (filterByEventGroups?.length) { - const groupIds = filterByEventGroups - .map((g) => (typeof g === 'object' ? g.id : g)) - .filter(Boolean) - if (groupIds.length) { - params.append('groups', groupIds.join(',')) - } - } - - if (filterByEventTags?.length) { - const tagIds = filterByEventTags - .map((t) => (typeof t === 'object' ? t.id : t)) - .filter(Boolean) - if (tagIds.length) { - params.append('tags', tagIds.join(',')) - } - } - - params.append('where[startDate][greater_than]', new Date().toISOString()) - - const response = await fetch(`/api/events?${params.toString()}`, { - cache: 'no-store', - }) - - if (!response.ok) { - throw new Error('Failed to fetch events') - } - - const data = await response.json() - setEvents(data.docs || []) - } catch (err) { - console.error('[useEvents Error]:', err) - setError(err instanceof Error ? err.message : 'An error occurred while fetching events') - } finally { - setLoading(false) - } - } - - fetchEvents() - }, [filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents, tenant]) - - return { events, loading, error } -} diff --git a/src/collections/Events/index.ts b/src/collections/Events/index.ts index 368e5714..77562e15 100644 --- a/src/collections/Events/index.ts +++ b/src/collections/Events/index.ts @@ -1,4 +1,4 @@ -import { accessByTenantRoleWithPermissiveRead } from '@/access/byTenantRole' +import { accessByTenantRole } from '@/access/byTenantRole' import { filterByTenant } from '@/access/filterByTenant' import { Banner } from '@/blocks/Banner/config' import { BlogListBlockLexical } from '@/blocks/BlogList/config' @@ -33,7 +33,7 @@ import { eventTypesData } from './constants' export const Events: CollectionConfig = { slug: 'events', - access: accessByTenantRoleWithPermissiveRead('events'), + access: accessByTenantRole('events'), admin: { baseListFilter: filterByTenant, group: 'Events', From fa39d786631130bb5348bebfc1d7916455960537 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 15:08:50 -0800 Subject: [PATCH 41/48] Update caching --- src/app/api/eventsQuery/route.ts | 9 ++++++++- src/collections/Events/index.ts | 8 +++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/app/api/eventsQuery/route.ts b/src/app/api/eventsQuery/route.ts index 93127c7c..b24bdeef 100644 --- a/src/app/api/eventsQuery/route.ts +++ b/src/app/api/eventsQuery/route.ts @@ -43,7 +43,14 @@ export async function GET(req: Request) { depth, }) - return Response.json({ docs: data.docs || [] }) + return Response.json( + { docs: data.docs || [] }, + { + headers: { + 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300', + }, + }, + ) } catch (error) { console.error('[Dynamic Events Endpoint Error]:', error) return Response.json( diff --git a/src/collections/Events/index.ts b/src/collections/Events/index.ts index 77562e15..709deeb0 100644 --- a/src/collections/Events/index.ts +++ b/src/collections/Events/index.ts @@ -27,6 +27,7 @@ import { InlineToolbarFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' +import { revalidatePath } from 'next/cache' import { CollectionConfig } from 'payload' import { populateBlocksInContent } from '../Posts/hooks/populateBlocksInContent' import { eventTypesData } from './constants' @@ -262,8 +263,13 @@ export const Events: CollectionConfig = { ], hooks: { beforeChange: [populatePublishedAt, populateBlocksInContent], - // TODO: need revalidation hooks here + // TODO: need revalidation hooks herehooks: { // TODO: need to update revalidation utilities to look for this blocksInContent field for relationships in addition to Posts and Home Pages + afterChange: [ + async () => { + await revalidatePath('/api/eventsQuery') + }, + ], }, versions: { drafts: { From ba362601c5bd4a6fa21ae60b95ed145e4c67cef9 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 15:13:54 -0800 Subject: [PATCH 42/48] Remove path revalidation --- src/collections/Events/index.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/collections/Events/index.ts b/src/collections/Events/index.ts index 709deeb0..a0f06f28 100644 --- a/src/collections/Events/index.ts +++ b/src/collections/Events/index.ts @@ -27,7 +27,6 @@ import { InlineToolbarFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' -import { revalidatePath } from 'next/cache' import { CollectionConfig } from 'payload' import { populateBlocksInContent } from '../Posts/hooks/populateBlocksInContent' import { eventTypesData } from './constants' @@ -265,11 +264,6 @@ export const Events: CollectionConfig = { beforeChange: [populatePublishedAt, populateBlocksInContent], // TODO: need revalidation hooks herehooks: { // TODO: need to update revalidation utilities to look for this blocksInContent field for relationships in addition to Posts and Home Pages - afterChange: [ - async () => { - await revalidatePath('/api/eventsQuery') - }, - ], }, versions: { drafts: { From a664a70772064f53153e7017c2b6a7725eff2013 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 15:29:08 -0800 Subject: [PATCH 43/48] Rename events endpoint --- src/app/api/{eventsQuery => events}/route.ts | 0 src/blocks/EventTable/Component.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/app/api/{eventsQuery => events}/route.ts (100%) diff --git a/src/app/api/eventsQuery/route.ts b/src/app/api/events/route.ts similarity index 100% rename from src/app/api/eventsQuery/route.ts rename to src/app/api/events/route.ts diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 1cf3767c..b0c63260 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -61,7 +61,7 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { } } - const response = await fetch(`/api/eventsQuery?${params.toString()}`, { + const response = await fetch(`/api/events?${params.toString()}`, { cache: 'no-store', }) From 5f2d83360abcf30893a22819ab999886421f772a Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Mon, 17 Nov 2025 22:05:04 -0800 Subject: [PATCH 44/48] WIP - fix events endpoint --- src/app/api/events/route.ts | 82 +++++++------------ src/blocks/EventTable/Component.tsx | 7 +- .../EventQuery/QueriedEventsComponent.tsx | 13 ++- src/utilities/queries/getEvents.ts | 2 +- 4 files changed, 38 insertions(+), 66 deletions(-) diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index b24bdeef..f9e1d424 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -1,61 +1,35 @@ -import config from '@/payload.config' -import { getPayload, Where } from 'payload' +import { getEvents } from '@/utilities/queries/getEvents' +import { NextRequest, NextResponse } from 'next/server' -export async function GET(req: Request) { - try { - const url = new URL(req.url) - const searchParams = url.searchParams +export async function GET(request: NextRequest) { + const searchParams = request.nextUrl.searchParams - const tenantId = searchParams.get('tenantId') - const maxEvents = parseInt(searchParams.get('limit') || '4') - const depth = parseInt(searchParams.get('depth') || '1') - const types = searchParams.get('types')?.split(',').filter(Boolean) - const groups = searchParams.get('groups')?.split(',').filter(Boolean) - const tags = searchParams.get('tags')?.split(',').filter(Boolean) - - if (!tenantId) { - return Response.json({ error: 'tenantId is required' }, { status: 400 }) - } - - const payload = await getPayload({ config }) - - const where: Where = { - tenant: { equals: tenantId }, - startDate: { greater_than: new Date().toISOString() }, - } - - if (types?.length) { - where.eventType = { in: types } - } - - if (groups?.length) { - where.eventGroup = { in: groups } - } + const parseArrayParam = (param: string | null): string[] | null => { + if (!param) return null + return param.split(',').filter(Boolean) + } - if (tags?.length) { - where.eventTags = { in: tags } - } + const queryParams = { + offset: searchParams.get('offset') ? Number(searchParams.get('offset')) : null, + limit: searchParams.get('limit') ? Number(searchParams.get('limit')) : null, + types: parseArrayParam(searchParams.get('types')), + startDate: searchParams.get('startDate'), + endDate: searchParams.get('endDate'), + groups: parseArrayParam(searchParams.get('groups')), + tags: parseArrayParam(searchParams.get('tags')), + modesOfTravel: parseArrayParam(searchParams.get('modesOfTravel')), + center: String(searchParams.get('tenantId')), + } - const data = await payload.find({ - collection: 'events', - where, - limit: maxEvents, - depth, - }) + const result = await getEvents(queryParams) - return Response.json( - { docs: data.docs || [] }, - { - headers: { - 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300', - }, - }, - ) - } catch (error) { - console.error('[Dynamic Events Endpoint Error]:', error) - return Response.json( - { error: error instanceof Error ? error.message : 'An error occurred' }, - { status: 500 }, - ) + if (result.error) { + return NextResponse.json(result, { status: 500 }) } + + return NextResponse.json(result, { + headers: { + 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300', + }, + }) } diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index b0c63260..4444fdd4 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -105,16 +105,15 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { )}
- {(loading || error) && ( + {loading || error ? (
{loading &&

Loading events...

} {error &&

Error loading events: {error}

}
- )} - {displayEvents && displayEvents.length > 0 ? ( + ) : displayEvents && displayEvents.length > 0 ? ( ) : ( -

There are no events matching these results.

+

There are no events matching these results.

)}
diff --git a/src/fields/EventQuery/QueriedEventsComponent.tsx b/src/fields/EventQuery/QueriedEventsComponent.tsx index d1e6d483..6c5351bd 100644 --- a/src/fields/EventQuery/QueriedEventsComponent.tsx +++ b/src/fields/EventQuery/QueriedEventsComponent.tsx @@ -54,9 +54,9 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr if (!tenantId) return const params = new URLSearchParams({ + tenantId: String(tenantId), limit: String(maxEvents || 4), depth: '1', - 'where[tenant][equals]': String(tenantId), }) if (sortBy) { @@ -71,7 +71,7 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr const groupIds = filterByEventGroups.filter(Boolean) if (groupIds.length > 0) { - params.append('where[type][in]', groupIds.join(',')) + params.append('groups', groupIds.join(',')) } } @@ -83,7 +83,7 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr const typeIds = filterByEventTypes.filter(Boolean) if (typeIds.length > 0) { - params.append('where[type][in]', typeIds.join(',')) + params.append('types', typeIds.join(',')) } } @@ -91,18 +91,17 @@ export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentPr const tagIds = filterByEventTags.filter(Boolean) if (tagIds.length > 0) { - params.append('where[tags][in]', tagIds.join(',')) + params.append('tags', tagIds.join(',')) } } - params.append('where[startDate][greater_than]', new Date().toISOString()) + // params.append('startDate', new Date().toISOString()) const response = await fetch(`/api/events?${params.toString()}`) if (!response.ok) { throw new Error('Failed to fetch events') } - const data = await response.json() - const events = data.docs || [] + const { events } = await response.json() setFetchedEvents(events) const options: OptionObject[] = events.map((event: Event) => ({ diff --git a/src/utilities/queries/getEvents.ts b/src/utilities/queries/getEvents.ts index 7e6264d7..034c566a 100644 --- a/src/utilities/queries/getEvents.ts +++ b/src/utilities/queries/getEvents.ts @@ -35,7 +35,7 @@ export async function getEvents(params: GetEventsParams): Promise Date: Wed, 19 Nov 2025 18:53:00 -0800 Subject: [PATCH 45/48] Consolidate events api route --- src/app/api/events/route.ts | 35 ----------------------------- src/blocks/EventTable/Component.tsx | 8 +++---- src/utilities/queries/getEvents.ts | 2 +- 3 files changed, 5 insertions(+), 40 deletions(-) delete mode 100644 src/app/api/events/route.ts diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts deleted file mode 100644 index f9e1d424..00000000 --- a/src/app/api/events/route.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { getEvents } from '@/utilities/queries/getEvents' -import { NextRequest, NextResponse } from 'next/server' - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams - - const parseArrayParam = (param: string | null): string[] | null => { - if (!param) return null - return param.split(',').filter(Boolean) - } - - const queryParams = { - offset: searchParams.get('offset') ? Number(searchParams.get('offset')) : null, - limit: searchParams.get('limit') ? Number(searchParams.get('limit')) : null, - types: parseArrayParam(searchParams.get('types')), - startDate: searchParams.get('startDate'), - endDate: searchParams.get('endDate'), - groups: parseArrayParam(searchParams.get('groups')), - tags: parseArrayParam(searchParams.get('tags')), - modesOfTravel: parseArrayParam(searchParams.get('modesOfTravel')), - center: String(searchParams.get('tenantId')), - } - - const result = await getEvents(queryParams) - - if (result.error) { - return NextResponse.json(result, { status: 500 }) - } - - return NextResponse.json(result, { - headers: { - 'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=300', - }, - }) -} diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 4444fdd4..6da00b53 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -30,11 +30,11 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { setLoading(true) setError(null) - const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id - if (!tenantId) return + const tenantSlug = typeof tenant === 'object' && tenant?.slug + if (!tenantSlug) return const params = new URLSearchParams({ - tenantId: String(tenantId), + center: tenantSlug, limit: String(maxEvents || 4), depth: '1', }) @@ -61,7 +61,7 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { } } - const response = await fetch(`/api/events?${params.toString()}`, { + const response = await fetch(`/api/${tenantSlug}/events?${params.toString()}`, { cache: 'no-store', }) diff --git a/src/utilities/queries/getEvents.ts b/src/utilities/queries/getEvents.ts index 034c566a..7e6264d7 100644 --- a/src/utilities/queries/getEvents.ts +++ b/src/utilities/queries/getEvents.ts @@ -35,7 +35,7 @@ export async function getEvents(params: GetEventsParams): Promise Date: Wed, 19 Nov 2025 19:25:53 -0800 Subject: [PATCH 46/48] Remove queriedEvents & add startDate to params --- src/app/(payload)/admin/importMap.js | 3 - src/blocks/EventList/Component.tsx | 99 ++++++++--- src/blocks/EventTable/Component.tsx | 4 +- src/endpoints/seed/blocks/event-list.ts | 1 - src/endpoints/seed/home-page.ts | 1 - src/endpoints/seed/pages/all-blocks-page.ts | 2 - .../EventQuery/QueriedEventsComponent.tsx | 166 ------------------ src/fields/EventQuery/config.ts | 14 +- src/payload-types.ts | 10 +- 9 files changed, 87 insertions(+), 213 deletions(-) delete mode 100644 src/fields/EventQuery/QueriedEventsComponent.tsx diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index a93becc5..a9b35ca7 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -22,7 +22,6 @@ import { AvyFxLogo as AvyFxLogo_f711e8d8656c7552b63fe9abc7b36dc4 } from '@/compo import { LogoutButton as LogoutButton_db9ac62598c46d0f1db201f6af05442e } from '@/components/LogoutButton' import { default as default_2aead22399b7847b21b134dc4a7931e0 } from '@/components/TenantSelector/TenantSelector' import { default as default_cb0ad5752e1389a2a940bb73c2c0e7d2 } from '@/components/ViewTypeAction' -import { QueriedEventsComponent as QueriedEventsComponent_134552ff11f6f98eabbe00772c3659e9 } from '@/fields/EventQuery/QueriedEventsComponent' import { LocationMap as LocationMap_4b1c9ff6af70dfec8b61ae82b54165d8 } from '@/fields/location/components/LocationMap' import { LinkLabelDescription as LinkLabelDescription_cc2cf53f1598892c0c926f3cb616a721 } from '@/fields/navLink/components/LinkLabelDescription' import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent' @@ -104,8 +103,6 @@ export const importMap = { '@/components/ColumnLayoutPicker#default': default_923dc5ccc0b72de4298251644cbfe39e, '@/blocks/Content/components/DefaultColumnAdder#DefaultColumnAdder': DefaultColumnAdder_006f8c6c8800e6fe3753b3785f2c4a01, - '@/fields/EventQuery/QueriedEventsComponent#QueriedEventsComponent': - QueriedEventsComponent_134552ff11f6f98eabbe00772c3659e9, '@payloadcms/plugin-seo/client#OverviewComponent': OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, '@payloadcms/plugin-seo/client#MetaImageComponent': diff --git a/src/blocks/EventList/Component.tsx b/src/blocks/EventList/Component.tsx index 16bd6392..75bb40c2 100644 --- a/src/blocks/EventList/Component.tsx +++ b/src/blocks/EventList/Component.tsx @@ -1,40 +1,95 @@ +'use client' + import { EventPreviewSmallRow } from '@/components/EventPreviewSmallRow' import RichText from '@/components/RichText' import { Button } from '@/components/ui/button' import type { Event, EventListBlock as EventListBlockProps } from '@/payload-types' +import { useTenant } from '@/providers/TenantProvider' import { cn } from '@/utilities/ui' +import { format } from 'date-fns' import Link from 'next/link' +import { useEffect, useState } from 'react' type EventListComponentProps = EventListBlockProps & { className?: string wrapInContainer?: boolean } -export const EventListBlockComponent = async (args: EventListComponentProps) => { - const { heading, belowHeadingContent, backgroundColor, className, wrapInContainer = true } = args +export const EventListBlockComponent = (args: EventListComponentProps) => { + const { + heading, + belowHeadingContent, + backgroundColor, + className, + wrapInContainer = true, + eventOptions, + } = args - const { filterByEventTypes, queriedEvents } = args.dynamicOptions || {} + const { filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents } = + args.dynamicOptions || {} const { staticEvents } = args.staticOptions || {} - let events = staticEvents?.filter( - (event): event is Event => typeof event === 'object' && event !== null, - ) + const { tenant } = useTenant() + const [fetchedEvents, setFetchedEvents] = useState([]) + const [eventParams, setEventParams] = useState('') - const hasStaticEvents = staticEvents && staticEvents.length > 0 + useEffect(() => { + if (eventOptions !== 'dynamic') return - if (!staticEvents || (staticEvents.length === 0 && queriedEvents && queriedEvents.length > 0)) { - events = queriedEvents?.filter( - (event): event is Event => typeof event === 'object' && event !== null, - ) - } + const fetchEvents = async () => { + const tenantSlug = typeof tenant === 'object' && tenant?.slug + if (!tenantSlug) return - const eventsLinkQueryParams = new URLSearchParams() + const params = new URLSearchParams({ + center: tenantSlug, + limit: String(maxEvents || 4), + depth: '1', + }) - if (filterByEventTypes && filterByEventTypes.length > 0) { - eventsLinkQueryParams.set('types', filterByEventTypes.join(',')) - } + if (filterByEventTypes?.length) { + params.append('types', filterByEventTypes.join(',')) + } + + if (filterByEventGroups?.length) { + const groupIds = filterByEventGroups + .map((g) => (typeof g === 'object' ? g.id : g)) + .filter(Boolean) + if (groupIds.length) { + params.append('groups', groupIds.join(',')) + } + } + + if (filterByEventTags?.length) { + const tagIds = filterByEventTags + .map((t) => (typeof t === 'object' ? t.id : t)) + .filter(Boolean) + if (tagIds.length) { + params.append('tags', tagIds.join(',')) + } + } + params.append('startDate', format(new Date(), 'MM-dd-yyyy')) + setEventParams(params.toString()) + + const response = await fetch(`/api/${tenantSlug}/events?${params.toString()}`, { + cache: 'no-store', + }) + + if (!response.ok) { + throw new Error('Failed to fetch events') + } + + const data = await response.json() + setFetchedEvents(data.events || []) + } + + fetchEvents() + }, [eventOptions, filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents, tenant]) + + let displayEvents + if (eventOptions === 'static') displayEvents = staticEvents as Event[] + if (eventOptions === 'dynamic') displayEvents = fetchedEvents - if (!events) { + if (!displayEvents) { return null } @@ -59,20 +114,20 @@ export const EventListBlockComponent = async (args: EventListComponentProps) =>
1 && '@3xl:grid-cols-2 @6xl:grid-cols-3', + displayEvents && displayEvents.length > 1 && '@3xl:grid-cols-2 @6xl:grid-cols-3', )} > - {events && events?.length > 0 ? ( - events?.map((event, index) => ( + {displayEvents && displayEvents?.length > 0 ? ( + displayEvents?.map((event, index) => ( )) ) : (

There are no events matching these results.

)}
- {!hasStaticEvents && ( + {eventOptions === 'static' && ( )} diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 6da00b53..25d1c775 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -5,6 +5,7 @@ import RichText from '@/components/RichText' import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' import { useTenant } from '@/providers/TenantProvider' import { cn } from '@/utilities/ui' +import { format } from 'date-fns' import { useEffect, useState } from 'react' type EventTableComponentProps = EventTableBlockProps & { @@ -60,6 +61,7 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { params.append('tags', tagIds.join(',')) } } + params.append('startDate', format(new Date(), 'MM-dd-yyyy')) const response = await fetch(`/api/${tenantSlug}/events?${params.toString()}`, { cache: 'no-store', @@ -70,7 +72,7 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { } const data = await response.json() - setFetchedEvents(data.docs || []) + setFetchedEvents(data.events || []) } catch (err) { console.error('[EventTable Error]:', err) setError(err instanceof Error ? err.message : 'An error occurred while fetching events') diff --git a/src/endpoints/seed/blocks/event-list.ts b/src/endpoints/seed/blocks/event-list.ts index a777c1cf..a4e56230 100644 --- a/src/endpoints/seed/blocks/event-list.ts +++ b/src/endpoints/seed/blocks/event-list.ts @@ -36,7 +36,6 @@ export const eventListBlock: EventListBlock = { eventOptions: 'dynamic', dynamicOptions: { maxEvents: 4, - queriedEvents: [], // Will be populated during seeding }, staticOptions: { staticEvents: [], // Will be populated with actual event references during seeding diff --git a/src/endpoints/seed/home-page.ts b/src/endpoints/seed/home-page.ts index 82421639..87656d45 100644 --- a/src/endpoints/seed/home-page.ts +++ b/src/endpoints/seed/home-page.ts @@ -162,7 +162,6 @@ export const homePage: ( eventOptions: 'dynamic', dynamicOptions: { maxEvents: 4, - queriedEvents: events.slice(0, 4).map((event) => event.id), // Use first 4 events for preview }, }, ], diff --git a/src/endpoints/seed/pages/all-blocks-page.ts b/src/endpoints/seed/pages/all-blocks-page.ts index 306cedab..393bae2f 100644 --- a/src/endpoints/seed/pages/all-blocks-page.ts +++ b/src/endpoints/seed/pages/all-blocks-page.ts @@ -64,7 +64,6 @@ export const allBlocksPage: ( eventOptions: 'dynamic', dynamicOptions: { maxEvents: 4, - queriedEvents: events.slice(0, 4).map((event) => event.id), // Use first 4 events for preview }, }, { @@ -89,7 +88,6 @@ export const allBlocksPage: ( dynamicOptions: { maxEvents: 6, - queriedEvents: [], // Will be populated during seeding }, staticOptions: { staticEvents: [], diff --git a/src/fields/EventQuery/QueriedEventsComponent.tsx b/src/fields/EventQuery/QueriedEventsComponent.tsx deleted file mode 100644 index 6c5351bd..00000000 --- a/src/fields/EventQuery/QueriedEventsComponent.tsx +++ /dev/null @@ -1,166 +0,0 @@ -'use client' - -import { Event } from '@/payload-types' -import { useTenantSelection } from '@/providers/TenantSelectionProvider/index.client' -import { FieldDescription, SelectInput, useField, useForm, useFormFields } from '@payloadcms/ui' -import { OptionObject } from 'payload' -import { useEffect, useState } from 'react' - -type QueriedEventsComponentProps = { - path: string - field?: { - label?: string - } -} - -export const QueriedEventsComponent = ({ path, field }: QueriedEventsComponentProps) => { - const label = field?.label || 'Queried Events' - const parentPathParts = path.split('.').slice(0, -1) - - const { value, setValue } = useField({ path }) - const [isLoading, setIsLoading] = useState(false) - const [fetchedEvents, setFetchedEvents] = useState([]) - const [selectOptions, setSelectOptions] = useState([]) - const { setDisabled } = useForm() - - const filterByEventGroups = useFormFields( - ([fields]) => fields[parentPathParts.concat(['filterByEventGroups']).join('.')]?.value, - ) - const filterByEventTypes = useFormFields( - ([fields]) => fields[parentPathParts.concat(['filterByEventTypes']).join('.')]?.value, - ) - const filterByEventTags = useFormFields( - ([fields]) => fields[parentPathParts.concat(['filterByEventTags']).join('.')]?.value, - ) - const sortBy = useFormFields( - ([fields]) => fields[parentPathParts.concat(['sortBy']).join('.')]?.value, - ) - const maxEvents = useFormFields( - ([fields]) => fields[parentPathParts.concat(['maxEvents']).join('.')]?.value, - ) - const { selectedTenantID: tenant } = useTenantSelection() - - useEffect(() => { - if (!tenant) { - return - } - - const fetchEvents = async () => { - setIsLoading(true) - setDisabled(true) - - try { - const tenantId = typeof tenant === 'number' ? tenant : (tenant as { id?: number })?.id - if (!tenantId) return - - const params = new URLSearchParams({ - tenantId: String(tenantId), - limit: String(maxEvents || 4), - depth: '1', - }) - - if (sortBy) { - params.append('sort', String(sortBy)) - } - - if ( - filterByEventGroups && - Array.isArray(filterByEventGroups) && - filterByEventGroups.length > 0 - ) { - const groupIds = filterByEventGroups.filter(Boolean) - - if (groupIds.length > 0) { - params.append('groups', groupIds.join(',')) - } - } - - if ( - filterByEventTypes && - Array.isArray(filterByEventTypes) && - filterByEventTypes.length > 0 - ) { - const typeIds = filterByEventTypes.filter(Boolean) - - if (typeIds.length > 0) { - params.append('types', typeIds.join(',')) - } - } - - if (filterByEventTags && Array.isArray(filterByEventTags) && filterByEventTags.length > 0) { - const tagIds = filterByEventTags.filter(Boolean) - - if (tagIds.length > 0) { - params.append('tags', tagIds.join(',')) - } - } - // params.append('startDate', new Date().toISOString()) - - const response = await fetch(`/api/events?${params.toString()}`) - if (!response.ok) { - throw new Error('Failed to fetch events') - } - - const { events } = await response.json() - setFetchedEvents(events) - - const options: OptionObject[] = events.map((event: Event) => ({ - label: event.title, - value: String(event.id), - })) - setSelectOptions(options) - - const currentEventIds = (value || []) - .map((event) => (typeof event === 'number' ? event : event.id)) - .sort() - const newEventIds = events.map((event: Event) => event.id).sort() - - if (JSON.stringify(currentEventIds) !== JSON.stringify(newEventIds)) { - setValue(events) - } - } catch (error) { - console.error('Error fetching events for EventList block:', error) - setFetchedEvents([]) - setSelectOptions([]) - setValue([]) - } finally { - setIsLoading(false) - setDisabled(false) - } - } - - fetchEvents() - }, [ - filterByEventGroups, - filterByEventTags, - filterByEventTypes, - maxEvents, - setDisabled, - setValue, - sortBy, - tenant, - value, - ]) - - const currentValue = fetchedEvents.map((event) => String(event.id)) - - return ( -
- - -
- ) -} diff --git a/src/fields/EventQuery/config.ts b/src/fields/EventQuery/config.ts index ba9430d2..758ff112 100644 --- a/src/fields/EventQuery/config.ts +++ b/src/fields/EventQuery/config.ts @@ -61,6 +61,7 @@ export const dynamicEventRelatedFields = (additionalFilters?: Field[]): Field[] type: 'group', admin: { condition: (_, siblingData) => siblingData?.eventOptions === 'dynamic', + description: 'Use Preview ↗ to see how events will appear', }, fields: [ ...(additionalFilters ?? []), @@ -115,19 +116,6 @@ export const dynamicEventRelatedFields = (additionalFilters?: Field[]): Field[] beforeValidate: [validateMaxEvents], }, }, - { - name: 'queriedEvents', - type: 'relationship', - label: 'Preview Events Order', - relationTo: 'events', - hasMany: true, - admin: { - readOnly: true, - components: { - Field: '@/fields/EventQuery/QueriedEventsComponent#QueriedEventsComponent', - }, - }, - }, ], }, ] diff --git a/src/payload-types.ts b/src/payload-types.ts index 70d4a93d..af8479aa 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -712,6 +712,9 @@ export interface EventListBlock { * Checking this will render the block with additional padding around it and using the background color you have selected. */ wrapInContainer?: boolean | null; + /** + * Use Preview ↗ to see how events will appear + */ dynamicOptions?: { /** * Optionally select event types to filter events. @@ -731,7 +734,6 @@ export interface EventListBlock { * Maximum number of events that will be displayed. Must be an integer. */ maxEvents?: number | null; - queriedEvents?: (number | Event)[] | null; }; staticOptions?: { /** @@ -981,6 +983,9 @@ export interface EventTableBlock { [k: string]: unknown; } | null; eventOptions: 'dynamic' | 'static'; + /** + * Use Preview ↗ to see how events will appear + */ dynamicOptions?: { /** * Optionally select event types to filter events. @@ -1000,7 +1005,6 @@ export interface EventTableBlock { * Maximum number of events that will be displayed. Must be an integer. */ maxEvents?: number | null; - queriedEvents?: (number | Event)[] | null; }; staticOptions?: { /** @@ -2917,7 +2921,6 @@ export interface EventListBlockSelect { filterByEventGroups?: T; filterByEventTags?: T; maxEvents?: T; - queriedEvents?: T; }; staticOptions?: | T @@ -2952,7 +2955,6 @@ export interface EventTableBlockSelect { filterByEventGroups?: T; filterByEventTags?: T; maxEvents?: T; - queriedEvents?: T; }; staticOptions?: | T From 5dfc45b7404885e47efee7652a931358aeb8771e Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Wed, 19 Nov 2025 23:24:56 -0800 Subject: [PATCH 47/48] Remove complicated logic --- src/blocks/EventTable/Component.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index 25d1c775..bc04ac49 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -20,7 +20,7 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { const { tenant } = useTenant() const [fetchedEvents, setFetchedEvents] = useState([]) - const [loading, setLoading] = useState(false) + const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { @@ -28,7 +28,6 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { const fetchEvents = async () => { try { - setLoading(true) setError(null) const tenantSlug = typeof tenant === 'object' && tenant?.slug @@ -112,10 +111,8 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { {loading &&

Loading events...

} {error &&

Error loading events: {error}

} - ) : displayEvents && displayEvents.length > 0 ? ( - ) : ( -

There are no events matching these results.

+ )} From 2b6deca4d33d39caac1cc99cb783ec915d370ee8 Mon Sep 17 00:00:00 2001 From: rchlfryn Date: Thu, 20 Nov 2025 20:01:21 -0800 Subject: [PATCH 48/48] Update events to check relationships --- src/blocks/EventList/Component.tsx | 9 ++++++--- src/blocks/EventTable/Component.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/blocks/EventList/Component.tsx b/src/blocks/EventList/Component.tsx index 75bb40c2..66bfaeb2 100644 --- a/src/blocks/EventList/Component.tsx +++ b/src/blocks/EventList/Component.tsx @@ -5,6 +5,7 @@ import RichText from '@/components/RichText' import { Button } from '@/components/ui/button' import type { Event, EventListBlock as EventListBlockProps } from '@/payload-types' import { useTenant } from '@/providers/TenantProvider' +import { filterValidPublishedRelationships } from '@/utilities/relationships' import { cn } from '@/utilities/ui' import { format } from 'date-fns' import Link from 'next/link' @@ -85,9 +86,11 @@ export const EventListBlockComponent = (args: EventListComponentProps) => { fetchEvents() }, [eventOptions, filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents, tenant]) - let displayEvents - if (eventOptions === 'static') displayEvents = staticEvents as Event[] - if (eventOptions === 'dynamic') displayEvents = fetchedEvents + let displayEvents: Event[] = filterValidPublishedRelationships(staticEvents) + + if (eventOptions === 'dynamic') { + displayEvents = filterValidPublishedRelationships(fetchedEvents) + } if (!displayEvents) { return null diff --git a/src/blocks/EventTable/Component.tsx b/src/blocks/EventTable/Component.tsx index bc04ac49..d776d7ae 100644 --- a/src/blocks/EventTable/Component.tsx +++ b/src/blocks/EventTable/Component.tsx @@ -4,6 +4,7 @@ import { EventTable } from '@/components/EventsTable' import RichText from '@/components/RichText' import type { Event, EventTableBlock as EventTableBlockProps } from '@/payload-types' import { useTenant } from '@/providers/TenantProvider' +import { filterValidPublishedRelationships } from '@/utilities/relationships' import { cn } from '@/utilities/ui' import { format } from 'date-fns' import { useEffect, useState } from 'react' @@ -83,9 +84,11 @@ export const EventTableBlockComponent = (args: EventTableComponentProps) => { fetchEvents() }, [eventOptions, filterByEventTypes, filterByEventGroups, filterByEventTags, maxEvents, tenant]) - let displayEvents - if (eventOptions === 'static') displayEvents = staticEvents as Event[] - if (eventOptions === 'dynamic') displayEvents = fetchedEvents + let displayEvents: Event[] = filterValidPublishedRelationships(staticEvents) + + if (eventOptions === 'dynamic') { + displayEvents = filterValidPublishedRelationships(fetchedEvents) + } if (!displayEvents) { return null