@eventcatalog/core 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +7 -3
  2. package/dist/analytics/analytics.cjs +1 -1
  3. package/dist/analytics/analytics.js +2 -2
  4. package/dist/analytics/log-build.cjs +1 -1
  5. package/dist/analytics/log-build.js +3 -3
  6. package/dist/catalog-to-astro-content-directory.cjs +1 -7
  7. package/dist/catalog-to-astro-content-directory.js +2 -2
  8. package/dist/{chunk-3EOBEGSB.js → chunk-2VGR4HMJ.js} +1 -1
  9. package/dist/{chunk-TFBAK5C5.js → chunk-CTL6CH3C.js} +1 -1
  10. package/dist/{chunk-7JDTB3U5.js → chunk-FIY5JLSQ.js} +0 -2
  11. package/dist/{chunk-IY5HYH2G.js → chunk-LMNJPHFP.js} +1 -1
  12. package/dist/{chunk-VCR3LHZR.js → chunk-R2NILSWL.js} +2 -6
  13. package/dist/{chunk-OW2FQPYP.js → chunk-WUCY3QHK.js} +1 -1
  14. package/dist/constants.cjs +1 -1
  15. package/dist/constants.js +1 -1
  16. package/dist/eventcatalog.cjs +2 -8
  17. package/dist/eventcatalog.js +6 -6
  18. package/dist/map-catalog-to-astro.cjs +0 -2
  19. package/dist/map-catalog-to-astro.js +1 -1
  20. package/dist/watcher.cjs +0 -2
  21. package/dist/watcher.js +2 -2
  22. package/eventcatalog/astro.config.mjs +0 -3
  23. package/eventcatalog/src/components/Grids/DomainGrid.tsx +233 -0
  24. package/eventcatalog/src/components/Grids/MessageGrid.tsx +457 -0
  25. package/eventcatalog/src/components/Grids/ServiceGrid.tsx +364 -0
  26. package/eventcatalog/src/components/Grids/components.tsx +170 -0
  27. package/eventcatalog/src/components/Grids/utils.tsx +38 -0
  28. package/eventcatalog/src/{content/config.ts → content.config.ts} +12 -2
  29. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +29 -17
  30. package/eventcatalog/src/pages/architecture/[type]/index.astro +88 -0
  31. package/eventcatalog/src/pages/chat/index.astro +1 -1
  32. package/eventcatalog/src/pages/docs/teams/[id]/index.astro +6 -13
  33. package/eventcatalog/src/pages/docs/users/[id]/index.astro +7 -13
  34. package/eventcatalog/src/pages/index.astro +237 -72
  35. package/eventcatalog/src/utils/url-builder.ts +20 -0
  36. package/eventcatalog/src/utils/users.ts +2 -2
  37. package/eventcatalog/tailwind.config.mjs +11 -0
  38. package/package.json +2 -1
  39. package/default-files-for-collections/teams.md +0 -11
  40. package/default-files-for-collections/users.md +0 -11
@@ -0,0 +1,364 @@
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { ServerIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
3
+ import { RectangleGroupIcon } from '@heroicons/react/24/outline';
4
+ import { buildUrl, buildUrlWithParams } from '@utils/url-builder';
5
+ import type { CollectionEntry } from 'astro:content';
6
+ import type { CollectionMessageTypes } from '@types';
7
+ import { getCollectionStyles } from './utils';
8
+ import { SearchBar, TypeFilters, Pagination } from './components';
9
+
10
+ interface ServiceGridProps {
11
+ services: CollectionEntry<'services'>[];
12
+ }
13
+
14
+ export default function ServiceGrid({ services }: ServiceGridProps) {
15
+ const [searchQuery, setSearchQuery] = useState('');
16
+ const [currentPage, setCurrentPage] = useState(1);
17
+ const [selectedTypes, setSelectedTypes] = useState<CollectionMessageTypes[]>([]);
18
+ const ITEMS_PER_PAGE = 16;
19
+ const [urlParams, setUrlParams] = useState<{
20
+ serviceIds?: string[];
21
+ domainId?: string;
22
+ domainName?: string;
23
+ serviceName?: string;
24
+ } | null>(null);
25
+
26
+ // Effect to sync URL params with state
27
+ useEffect(() => {
28
+ const params = new URLSearchParams(window.location.search);
29
+ const serviceIds = params.get('serviceIds')?.split(',').filter(Boolean);
30
+ const domainId = params.get('domainId') || undefined;
31
+ const domainName = params.get('domainName') || undefined;
32
+ const serviceName = params.get('serviceName') || undefined;
33
+ setUrlParams({
34
+ serviceIds,
35
+ domainId,
36
+ domainName,
37
+ serviceName,
38
+ });
39
+ }, []);
40
+
41
+ const filteredAndSortedServices = useMemo(() => {
42
+ // Don't filter until we have URL params
43
+ if (urlParams === null) return [];
44
+
45
+ let result = [...services];
46
+
47
+ // Filter by service IDs if present
48
+ if (urlParams.serviceIds?.length) {
49
+ result = result.filter(
50
+ (service) => urlParams.serviceIds?.includes(service.data.id) && !service.data.id.includes('/versioned/')
51
+ );
52
+ }
53
+
54
+ // Filter by search query
55
+ if (searchQuery) {
56
+ const query = searchQuery.toLowerCase();
57
+ result = result.filter(
58
+ (service) =>
59
+ service.data.name?.toLowerCase().includes(query) ||
60
+ service.data.summary?.toLowerCase().includes(query) ||
61
+ service.data.sends?.some((message: any) => message.data.name.toLowerCase().includes(query)) ||
62
+ service.data.receives?.some((message: any) => message.data.name.toLowerCase().includes(query))
63
+ );
64
+ }
65
+
66
+ // Filter by selected message types
67
+ if (selectedTypes.length > 0) {
68
+ result = result.filter((service) => {
69
+ const hasMatchingSends = service.data.sends?.some((message: any) => selectedTypes.includes(message.collection));
70
+ const hasMatchingReceives = service.data.receives?.some((message: any) => selectedTypes.includes(message.collection));
71
+ return hasMatchingSends || hasMatchingReceives;
72
+ });
73
+ }
74
+
75
+ // Sort by name by default
76
+ result.sort((a, b) => (a.data.name || a.data.id).localeCompare(b.data.name || b.data.id));
77
+
78
+ return result;
79
+ }, [services, searchQuery, urlParams, selectedTypes]);
80
+
81
+ // Add pagination calculation
82
+ const paginatedServices = useMemo(() => {
83
+ if (urlParams?.domainId || urlParams?.serviceIds?.length) {
84
+ return filteredAndSortedServices;
85
+ }
86
+
87
+ const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
88
+ return filteredAndSortedServices.slice(startIndex, startIndex + ITEMS_PER_PAGE);
89
+ }, [filteredAndSortedServices, currentPage, urlParams]);
90
+
91
+ const totalPages = useMemo(() => {
92
+ if (urlParams?.domainId || urlParams?.serviceIds?.length) return 1;
93
+ return Math.ceil(filteredAndSortedServices.length / ITEMS_PER_PAGE);
94
+ }, [filteredAndSortedServices.length, urlParams]);
95
+
96
+ // Reset pagination when search query or filters change
97
+ useEffect(() => {
98
+ setCurrentPage(1);
99
+ }, [searchQuery, selectedTypes]);
100
+
101
+ return (
102
+ <div>
103
+ {/* Breadcrumb */}
104
+ <nav className="mb-4 flex items-center space-x-2 text-sm text-gray-500">
105
+ <a href={buildUrl('/architecture/domains')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
106
+ <RectangleGroupIcon className="h-4 w-4" />
107
+ Domains
108
+ </a>
109
+ <ChevronRightIcon className="h-4 w-4" />
110
+ <a href={buildUrl('/architecture/services')} className="hover:text-gray-700 hover:underline flex items-center gap-2">
111
+ <ServerIcon className="h-4 w-4" />
112
+ Services
113
+ </a>
114
+ {urlParams?.domainId && (
115
+ <>
116
+ <ChevronRightIcon className="h-4 w-4" />
117
+ <span className="text-gray-900">{urlParams.domainId}</span>
118
+ </>
119
+ )}
120
+ </nav>
121
+
122
+ {/* Title Section */}
123
+ <div className="relative border-b border-gray-200 mb-4 pb-4">
124
+ <div className="md:flex md:items-start md:justify-between">
125
+ <div className="min-w-0 flex-1 max-w-lg">
126
+ <div className="flex items-center gap-2">
127
+ <h1 className="text-2xl font-bold leading-7 text-gray-900 sm:text-3xl sm:truncate">
128
+ {urlParams?.domainId
129
+ ? `Services in the ${urlParams.domainName} domain (${filteredAndSortedServices.length})`
130
+ : 'All Services'}
131
+ </h1>
132
+ </div>
133
+ <p className="mt-2 text-sm text-gray-500">
134
+ {urlParams?.domainId
135
+ ? `Browse services in the ${urlParams.domainId} domain`
136
+ : 'Browse and discover services in your event-driven architecture'}
137
+ </p>
138
+ </div>
139
+
140
+ <div className="mt-6 md:mt-0 md:ml-4 flex-shrink-0">
141
+ <SearchBar
142
+ searchQuery={searchQuery}
143
+ onSearchChange={setSearchQuery}
144
+ placeholder="Search services by name, summary, or messages..."
145
+ totalResults={filteredAndSortedServices.length}
146
+ totalItems={services.length}
147
+ />
148
+ </div>
149
+ </div>
150
+ </div>
151
+
152
+ <div className="mb-8">
153
+ {/* Results count and pagination */}
154
+ <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
155
+ <TypeFilters
156
+ selectedTypes={selectedTypes}
157
+ onTypeChange={setSelectedTypes}
158
+ filteredCount={filteredAndSortedServices.length}
159
+ totalCount={services.length}
160
+ />
161
+ <div className="text-sm text-gray-500">
162
+ {urlParams?.domainId || urlParams?.serviceIds?.length ? (
163
+ <span>
164
+ Showing {filteredAndSortedServices.length} services in the {urlParams.domainId} domain
165
+ </span>
166
+ ) : (
167
+ <span>
168
+ Showing {(currentPage - 1) * ITEMS_PER_PAGE + 1} to{' '}
169
+ {Math.min(currentPage * ITEMS_PER_PAGE, filteredAndSortedServices.length)} of {filteredAndSortedServices.length}{' '}
170
+ services
171
+ </span>
172
+ )}
173
+ </div>
174
+ {!(urlParams?.domainId || urlParams?.serviceIds?.length) && (
175
+ <Pagination
176
+ currentPage={currentPage}
177
+ totalPages={totalPages}
178
+ totalItems={filteredAndSortedServices.length}
179
+ itemsPerPage={ITEMS_PER_PAGE}
180
+ onPageChange={setCurrentPage}
181
+ />
182
+ )}
183
+ </div>
184
+ </div>
185
+
186
+ {filteredAndSortedServices.length > 0 && (
187
+ <div className={`rounded-xl overflow-hidden ${urlParams?.domainId ? 'bg-yellow-50 p-8 border-2 border-yellow-400' : ''}`}>
188
+ {urlParams?.domainName && (
189
+ <>
190
+ <div className="mb-6 flex items-center justify-between">
191
+ <div className="flex items-center gap-2">
192
+ <RectangleGroupIcon className="h-5 w-5 text-yellow-500" />
193
+ <span className="text-2xl font-semibold text-gray-900">{urlParams.domainName}</span>
194
+ </div>
195
+ <div className="flex gap-2">
196
+ <a
197
+ href={buildUrl(`/visualiser/domains/${urlParams.domainId}`)}
198
+ className="inline-flex items-center px-3 py-2 text-sm font-medium bg-white border border-gray-300 rounded-md transition-colors duration-200"
199
+ >
200
+ View in visualizer
201
+ </a>
202
+ <a
203
+ href={buildUrl(`/docs/domains/${urlParams.domainId}`)}
204
+ className="inline-flex items-center px-3 py-2 text-sm font-medium text-black border border-gray-300 bg-white rounded-md transition-colors duration-200"
205
+ >
206
+ Read documentation
207
+ </a>
208
+ </div>
209
+ </div>
210
+ </>
211
+ )}
212
+
213
+ <div className="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 gap-6">
214
+ {paginatedServices.map((service) => {
215
+ return (
216
+ <a
217
+ key={service.data.id}
218
+ href={buildUrlWithParams('/architecture/messages', {
219
+ serviceName: service.data.name,
220
+ serviceId: service.data.id,
221
+ domainId: urlParams?.domainId,
222
+ domainName: urlParams?.domainName,
223
+ })}
224
+ className="group hover:bg-pink-50 bg-white border-2 border-dashed border-pink-400 rounded-lg shadow-sm hover:shadow-lg transition-all duration-200 overflow-hidden"
225
+ >
226
+ <div className="p-6">
227
+ <div className="flex items-center justify-between mb-3">
228
+ <div className="flex items-center gap-2">
229
+ <ServerIcon className="h-5 w-5 text-pink-500" />
230
+ <h3 className="text-lg font-semibold text-gray-900 truncate group-hover:underline transition-colors duration-200">
231
+ {service.data.name || service.data.id} (v{service.data.version})
232
+ </h3>
233
+ </div>
234
+ </div>
235
+
236
+ {service.data.summary && (
237
+ <p className="text-gray-600 text-sm line-clamp-2 min-h-[2.5rem]">{service.data.summary}</p>
238
+ )}
239
+
240
+ <div className="space-y-4">
241
+ {/* Messages Section */}
242
+ {!urlParams?.serviceName && (
243
+ <div className="flex items-center gap-4">
244
+ <div className="flex-1 h-full flex flex-col bg-blue-100 border border-blue-300 rounded-lg p-4">
245
+ <div className="space-y-2 flex-1">
246
+ {service.data.receives
247
+ ?.filter(
248
+ (message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection)
249
+ )
250
+ ?.map((message: any) => {
251
+ const { Icon, color } = getCollectionStyles(message.collection);
252
+ return (
253
+ <a
254
+ key={message.data.name}
255
+ href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
256
+ className="group flex border border-gray-200 items-center gap-1 rounded-md text-[11px] font-medium hover:bg-gray-50 transition-colors duration-200 bg-white"
257
+ >
258
+ <div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
259
+ <Icon className={`h-3 w-3 text-${color}-500`} />
260
+ </div>
261
+ <span className="px-1 py-1">{message.data.name}</span>
262
+ </a>
263
+ );
264
+ })}
265
+ {(!service.data.receives?.length ||
266
+ (selectedTypes.length > 0 &&
267
+ !service.data.receives?.some((message: any) =>
268
+ selectedTypes.includes(message.collection)
269
+ ))) && (
270
+ <div className="text-center py-4">
271
+ <p className="text-gray-500 text-[10px]">
272
+ {selectedTypes.length > 0
273
+ ? `Service does not receive ${selectedTypes.join(' or ')}`
274
+ : 'Service does not receive any messages'}
275
+ </p>
276
+ </div>
277
+ )}
278
+ </div>
279
+ </div>
280
+
281
+ <div className="flex items-center gap-2">
282
+ <div className="w-4 h-[2px] bg-blue-200"></div>
283
+ <div className="bg-white border-2 border-pink-100 rounded-lg p-4 shadow-sm">
284
+ <div className="flex flex-col items-center gap-3">
285
+ <ServerIcon className="h-8 w-8 text-pink-500" />
286
+ <div className="text-center">
287
+ <p className="text-sm font-medium text-gray-900">{service.data.name || service.data.id}</p>
288
+ <p className="text-xs text-gray-500">v{service.data.version}</p>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ <div className="w-4 h-[2px] bg-emerald-200"></div>
293
+ </div>
294
+
295
+ <div className="flex-1 h-full flex flex-col bg-green-100 border border-green-300 rounded-lg p-4">
296
+ <div className="space-y-2 flex-1">
297
+ {service.data.sends
298
+ ?.filter(
299
+ (message: any) => selectedTypes.length === 0 || selectedTypes.includes(message.collection)
300
+ )
301
+ ?.map((message: any) => {
302
+ const { Icon, color } = getCollectionStyles(message.collection);
303
+ return (
304
+ <a
305
+ key={message.data.name}
306
+ href={buildUrl(`/docs/${message.collection}/${message.data.id}/${message.data.version}`)}
307
+ className="group flex border border-gray-200 items-center gap-1 rounded-md text-[11px] font-medium hover:bg-gray-50 transition-colors duration-200 bg-white"
308
+ >
309
+ <div className="bg-white border-r border-gray-200 px-2 py-1.5 rounded-l-md">
310
+ <Icon className={`h-3 w-3 text-${color}-500`} />
311
+ </div>
312
+ <span className="px-1 py-1">{message.data.name}</span>
313
+ </a>
314
+ );
315
+ })}
316
+ {(!service.data.sends?.length ||
317
+ (selectedTypes.length > 0 &&
318
+ !service.data.sends?.some((message: any) => selectedTypes.includes(message.collection)))) && (
319
+ <div className="text-center py-4 ">
320
+ <p className="text-gray-500 text-[10px]">
321
+ {selectedTypes.length > 0
322
+ ? `Service does not send ${selectedTypes.join(' or ')}`
323
+ : 'Service does not send any messages'}
324
+ </p>
325
+ </div>
326
+ )}
327
+ </div>
328
+ </div>
329
+ </div>
330
+ )}
331
+ </div>
332
+ </div>
333
+ </a>
334
+ );
335
+ })}
336
+ </div>
337
+ </div>
338
+ )}
339
+
340
+ {filteredAndSortedServices.length === 0 && (
341
+ <div className="text-center py-12 bg-gray-50 rounded-lg">
342
+ <p className="text-gray-500 text-lg">
343
+ {selectedTypes.length > 0
344
+ ? `No services found that ${selectedTypes.length > 1 ? 'handle' : 'handles'} ${selectedTypes.join(' or ')} messages`
345
+ : 'No services found matching your criteria'}
346
+ </p>
347
+ </div>
348
+ )}
349
+
350
+ {/* Bottom pagination */}
351
+ {!(urlParams?.domainId || urlParams?.serviceIds?.length) && (
352
+ <div className="mt-8 border-t border-gray-200">
353
+ <Pagination
354
+ currentPage={currentPage}
355
+ totalPages={totalPages}
356
+ totalItems={filteredAndSortedServices.length}
357
+ itemsPerPage={ITEMS_PER_PAGE}
358
+ onPageChange={setCurrentPage}
359
+ />
360
+ </div>
361
+ )}
362
+ </div>
363
+ );
364
+ }
@@ -0,0 +1,170 @@
1
+ import React from 'react';
2
+ import {
3
+ MagnifyingGlassIcon,
4
+ ChevronLeftIcon,
5
+ ChevronRightIcon,
6
+ ChevronDoubleLeftIcon,
7
+ ChevronDoubleRightIcon,
8
+ } from '@heroicons/react/24/outline';
9
+ import type { CollectionMessageTypes } from '@types';
10
+ import { getCollectionStyles, type PaginationProps, type SearchBarProps, type TypeFilterProps } from './utils';
11
+
12
+ export function SearchBar({ searchQuery, onSearchChange, placeholder, totalResults, totalItems }: SearchBarProps) {
13
+ return (
14
+ <div className="w-full md:w-96">
15
+ <div className="relative">
16
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
17
+ <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
18
+ </div>
19
+ <input
20
+ type="text"
21
+ placeholder={placeholder || 'Search...'}
22
+ value={searchQuery}
23
+ onChange={(e) => onSearchChange(e.target.value)}
24
+ className="block w-full rounded-lg border-0 py-2.5 pl-10 pr-4 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary sm:text-sm sm:leading-6"
25
+ />
26
+ {searchQuery && (
27
+ <div className="absolute inset-y-0 right-0 flex items-center pr-3">
28
+ <button onClick={() => onSearchChange('')} className="text-gray-400 hover:text-gray-500 focus:outline-none">
29
+ <span className="sr-only">Clear search</span>
30
+ <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
31
+ <path
32
+ fillRule="evenodd"
33
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
34
+ clipRule="evenodd"
35
+ />
36
+ </svg>
37
+ </button>
38
+ </div>
39
+ )}
40
+ </div>
41
+ {searchQuery && totalResults !== undefined && totalItems !== undefined && (
42
+ <div className="mt-2 text-sm text-gray-500 flex items-center justify-between">
43
+ <span>
44
+ Found <span className="font-medium text-gray-900">{totalResults}</span> of{' '}
45
+ <span className="font-medium text-gray-900">{totalItems}</span>
46
+ </span>
47
+ <span className="text-gray-400 text-xs">ESC to clear</span>
48
+ </div>
49
+ )}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export function TypeFilters({ selectedTypes, onTypeChange, filteredCount, totalCount }: TypeFilterProps) {
55
+ const types: CollectionMessageTypes[] = ['events', 'commands', 'queries'];
56
+
57
+ return (
58
+ <div className="flex items-center gap-2">
59
+ {types.map((type) => {
60
+ const { color, Icon } = getCollectionStyles(type);
61
+ const isSelected = selectedTypes.includes(type);
62
+ return (
63
+ <button
64
+ key={type}
65
+ onClick={() => {
66
+ onTypeChange(selectedTypes.includes(type) ? selectedTypes.filter((t) => t !== type) : [...selectedTypes, type]);
67
+ }}
68
+ className={`
69
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium
70
+ transition-colors duration-200
71
+ ${
72
+ isSelected
73
+ ? `bg-${color}-100 text-${color}-700 ring-2 ring-${color}-500`
74
+ : 'bg-gray-50 text-gray-600 hover:bg-gray-100'
75
+ }
76
+ `}
77
+ >
78
+ <Icon className={`h-4 w-4 ${isSelected ? `text-${color}-500` : 'text-gray-400'}`} />
79
+ <span className="capitalize">{type}</span>
80
+ {isSelected && filteredCount !== undefined && (
81
+ <span
82
+ className={`inline-flex items-center justify-center px-2 py-0.5 text-xs font-medium bg-${color}-50 text-${color}-700 rounded-full`}
83
+ >
84
+ {filteredCount}
85
+ </span>
86
+ )}
87
+ </button>
88
+ );
89
+ })}
90
+ {selectedTypes.length > 0 && (
91
+ <button onClick={() => onTypeChange([])} className="text-xs text-gray-500 hover:text-gray-700 hover:underline">
92
+ Clear filters
93
+ </button>
94
+ )}
95
+ </div>
96
+ );
97
+ }
98
+
99
+ export function Pagination({ currentPage, totalPages, totalItems, itemsPerPage, onPageChange }: PaginationProps) {
100
+ if (totalPages <= 1) return null;
101
+
102
+ return (
103
+ <div className="flex items-center justify-between border-gray-200 bg-white px-4 py-3 sm:px-6">
104
+ <div className="flex flex-1 justify-between sm:hidden">
105
+ <button
106
+ onClick={() => onPageChange(Math.max(1, currentPage - 1))}
107
+ disabled={currentPage === 1}
108
+ className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
109
+ >
110
+ Previous
111
+ </button>
112
+ <button
113
+ onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
114
+ disabled={currentPage === totalPages}
115
+ className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
116
+ >
117
+ Next
118
+ </button>
119
+ </div>
120
+ <div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
121
+ <div className="pr-4">
122
+ <p className="text-sm text-gray-700">
123
+ Showing <span className="font-medium">{(currentPage - 1) * itemsPerPage + 1}</span> to{' '}
124
+ <span className="font-medium">{Math.min(currentPage * itemsPerPage, totalItems)}</span> of{' '}
125
+ <span className="font-medium">{totalItems}</span> results
126
+ </p>
127
+ </div>
128
+ <div>
129
+ <nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
130
+ <button
131
+ onClick={() => onPageChange(1)}
132
+ disabled={currentPage === 1}
133
+ className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
134
+ >
135
+ <span className="sr-only">First</span>
136
+ <ChevronDoubleLeftIcon className="h-5 w-5" aria-hidden="true" />
137
+ </button>
138
+ <button
139
+ onClick={() => onPageChange(Math.max(1, currentPage - 1))}
140
+ disabled={currentPage === 1}
141
+ className="relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
142
+ >
143
+ <span className="sr-only">Previous</span>
144
+ <ChevronLeftIcon className="h-5 w-5" aria-hidden="true" />
145
+ </button>
146
+ <span className="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 focus:outline-offset-0">
147
+ Page {currentPage} of {totalPages}
148
+ </span>
149
+ <button
150
+ onClick={() => onPageChange(Math.min(totalPages, currentPage + 1))}
151
+ disabled={currentPage === totalPages}
152
+ className="relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
153
+ >
154
+ <span className="sr-only">Next</span>
155
+ <ChevronRightIcon className="h-5 w-5" aria-hidden="true" />
156
+ </button>
157
+ <button
158
+ onClick={() => onPageChange(totalPages)}
159
+ disabled={currentPage === totalPages}
160
+ className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
161
+ >
162
+ <span className="sr-only">Last</span>
163
+ <ChevronDoubleRightIcon className="h-5 w-5" aria-hidden="true" />
164
+ </button>
165
+ </nav>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
@@ -0,0 +1,38 @@
1
+ import { BoltIcon, ChatBubbleLeftIcon, MagnifyingGlassIcon, EnvelopeIcon } from '@heroicons/react/24/outline';
2
+ import type { CollectionMessageTypes } from '@types';
3
+
4
+ export const getCollectionStyles = (collection: CollectionMessageTypes) => {
5
+ switch (collection) {
6
+ case 'events':
7
+ return { color: 'orange', Icon: BoltIcon };
8
+ case 'commands':
9
+ return { color: 'blue', Icon: ChatBubbleLeftIcon };
10
+ case 'queries':
11
+ return { color: 'green', Icon: MagnifyingGlassIcon };
12
+ default:
13
+ return { color: 'gray', Icon: EnvelopeIcon };
14
+ }
15
+ };
16
+
17
+ export interface PaginationProps {
18
+ currentPage: number;
19
+ totalPages: number;
20
+ totalItems: number;
21
+ itemsPerPage: number;
22
+ onPageChange: (page: number) => void;
23
+ }
24
+
25
+ export interface SearchBarProps {
26
+ searchQuery: string;
27
+ onSearchChange: (query: string) => void;
28
+ placeholder?: string;
29
+ totalResults?: number;
30
+ totalItems?: number;
31
+ }
32
+
33
+ export interface TypeFilterProps {
34
+ selectedTypes: CollectionMessageTypes[];
35
+ onTypeChange: (types: CollectionMessageTypes[]) => void;
36
+ filteredCount?: number;
37
+ totalCount?: number;
38
+ }
@@ -1,4 +1,6 @@
1
1
  import { z, defineCollection, reference } from 'astro:content';
2
+ import { glob } from 'astro/loaders';
3
+ import { join } from 'node:path';
2
4
 
3
5
  const badge = z.object({
4
6
  content: z.string(),
@@ -252,8 +254,16 @@ const ubiquitousLanguages = defineCollection({
252
254
  }),
253
255
  });
254
256
 
257
+ const projectDirBase = (() => {
258
+ if (process.platform === 'win32') {
259
+ const projectDirPath = process.env.PROJECT_DIR!.replace(/\\/g, '/');
260
+ return projectDirPath.startsWith('/') ? projectDirPath : `/${projectDirPath}`;
261
+ }
262
+ return process.env.PROJECT_DIR;
263
+ })();
264
+
255
265
  const users = defineCollection({
256
- type: 'content',
266
+ loader: glob({ pattern: 'users/*.md', base: projectDirBase, generateId: ({ data }) => data.id as string }),
257
267
  schema: z.object({
258
268
  id: z.string(),
259
269
  name: z.string(),
@@ -274,7 +284,7 @@ const users = defineCollection({
274
284
  });
275
285
 
276
286
  const teams = defineCollection({
277
- type: 'content',
287
+ loader: glob({ pattern: 'teams/*.md', base: projectDirBase, generateId: ({ data }) => data.id as string }),
278
288
  schema: z.object({
279
289
  id: z.string(),
280
290
  name: z.string(),
@@ -9,6 +9,7 @@ import { BookOpenText, Workflow, TableProperties, House, BookUser, MessageSquare
9
9
  import Header from '../components/Header.astro';
10
10
  import SEO from '../components/Seo.astro';
11
11
  import SideNav from '../components/SideNav/SideNav.astro';
12
+ import '@fontsource/inter';
12
13
 
13
14
  import { getCommands } from '@utils/commands';
14
15
  import { getDomains } from '@utils/collections/domains';
@@ -92,6 +93,15 @@ const navigationItems = [
92
93
  current: currentPath.includes('/directory'),
93
94
  sidebar: false,
94
95
  },
96
+ {
97
+ id: '/architecture',
98
+ label: 'Architecture',
99
+ icon: BookUser,
100
+ href: buildUrl('/architecture/domains'),
101
+ current: currentPath.includes('/architecture'),
102
+ sidebar: false,
103
+ hidden: true,
104
+ },
95
105
  {
96
106
  id: '/chat',
97
107
  label: 'AI Assistant',
@@ -139,23 +149,25 @@ const canPageBeEmbedded = process.env.ENABLE_EMBED === 'true';
139
149
  <nav class="flex flex-col h-[84vh] justify-between">
140
150
  <div class="flex flex-col items-center flex-1 space-y-8">
141
151
  {
142
- navigationItems.map((item) => {
143
- return (
144
- <a
145
- id={item.id}
146
- data-role="nav-item"
147
- href={item.href}
148
- class={`p-1.5 inline-block transition-colors duration-200 rounded-lg ${item.current ? 'text-white bg-gradient-to-b from-purple-500 to-purple-700' : 'hover:bg-gradient-to-r hover:from-purple-500 hover:to-purple-700 hover:text-white text-gray-700'}`}
149
- >
150
- <div class="has-tooltip">
151
- <span class="tooltip rounded shadow-lg p-1 text-xs bg-gradient-to-l from-purple-500 to-purple-700 text-white ml-10">
152
- {item.label}
153
- </span>
154
- <item.icon className="h-6 w-6 " />
155
- </div>
156
- </a>
157
- );
158
- })
152
+ navigationItems
153
+ .filter((item) => !item.hidden)
154
+ .map((item) => {
155
+ return (
156
+ <a
157
+ id={item.id}
158
+ data-role="nav-item"
159
+ href={item.href}
160
+ class={`p-1.5 inline-block transition-colors duration-200 rounded-lg ${item.current ? 'text-white bg-gradient-to-b from-purple-500 to-purple-700' : 'hover:bg-gradient-to-r hover:from-purple-500 hover:to-purple-700 hover:text-white text-gray-700'}`}
161
+ >
162
+ <div class="has-tooltip">
163
+ <span class="tooltip rounded shadow-lg p-1 text-xs bg-gradient-to-l from-purple-500 to-purple-700 text-white ml-10">
164
+ {item.label}
165
+ </span>
166
+ <item.icon className="h-6 w-6 " />
167
+ </div>
168
+ </a>
169
+ );
170
+ })
159
171
  }
160
172
  </div>
161
173
  </nav>